1#[cfg(target_os = "macos")]
7use directories::BaseDirs;
8use directories::ProjectDirs;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14use crate::credential::CredentialStore;
15use crate::error::{ConfigError, Result};
16
17#[derive(Debug, Serialize, Deserialize, Default, Clone)]
19pub struct Config {
20 #[serde(default, rename = "default_enterprise")]
22 pub default_enterprise: Option<String>,
23 #[serde(default, rename = "default_cloud")]
25 pub default_cloud: Option<String>,
26 #[serde(default, rename = "default_database")]
28 pub default_database: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub files_api_key: Option<String>,
33 #[serde(default)]
35 pub profiles: HashMap<String, Profile>,
36}
37
38#[derive(Debug, Serialize, Deserialize, Clone)]
40pub struct Profile {
41 pub deployment_type: DeploymentType,
43 #[serde(flatten)]
45 pub credentials: ProfileCredentials,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub files_api_key: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub resilience: Option<crate::ResilienceConfig>,
53}
54
55#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, clap::ValueEnum)]
57#[serde(rename_all = "lowercase")]
58pub enum DeploymentType {
59 Cloud,
60 Enterprise,
61 Database,
62}
63
64#[derive(Debug, Serialize, Deserialize, Clone)]
66#[serde(untagged)]
67pub enum ProfileCredentials {
68 Cloud {
69 api_key: String,
70 api_secret: String,
71 #[serde(default = "default_cloud_url")]
72 api_url: String,
73 },
74 Enterprise {
75 url: String,
76 username: String,
77 password: Option<String>, #[serde(default)]
79 insecure: bool,
80 },
81 Database {
82 host: String,
83 port: u16,
84 #[serde(default)]
85 password: Option<String>,
86 #[serde(default = "default_tls")]
87 tls: bool,
88 #[serde(default = "default_username")]
89 username: String,
90 #[serde(default)]
91 database: u8,
92 },
93}
94
95impl std::fmt::Display for DeploymentType {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 DeploymentType::Cloud => write!(f, "cloud"),
99 DeploymentType::Enterprise => write!(f, "enterprise"),
100 DeploymentType::Database => write!(f, "database"),
101 }
102 }
103}
104
105fn default_tls() -> bool {
106 true
107}
108
109fn default_username() -> String {
110 "default".to_string()
111}
112
113impl Profile {
114 pub fn cloud_credentials(&self) -> Option<(&str, &str, &str)> {
116 match &self.credentials {
117 ProfileCredentials::Cloud {
118 api_key,
119 api_secret,
120 api_url,
121 } => Some((api_key.as_str(), api_secret.as_str(), api_url.as_str())),
122 _ => None,
123 }
124 }
125
126 pub fn enterprise_credentials(&self) -> Option<(&str, &str, Option<&str>, bool)> {
128 match &self.credentials {
129 ProfileCredentials::Enterprise {
130 url,
131 username,
132 password,
133 insecure,
134 } => Some((
135 url.as_str(),
136 username.as_str(),
137 password.as_deref(),
138 *insecure,
139 )),
140 _ => None,
141 }
142 }
143
144 pub fn has_password(&self) -> bool {
146 matches!(
147 self.credentials,
148 ProfileCredentials::Enterprise {
149 password: Some(_),
150 ..
151 } | ProfileCredentials::Database {
152 password: Some(_),
153 ..
154 }
155 )
156 }
157
158 pub fn resolve_cloud_credentials(&self) -> Result<Option<(String, String, String)>> {
160 match &self.credentials {
161 ProfileCredentials::Cloud {
162 api_key,
163 api_secret,
164 api_url,
165 } => {
166 let store = CredentialStore::new();
167
168 let resolved_key = store
170 .get_credential(api_key, Some("REDIS_CLOUD_API_KEY"))
171 .map_err(|e| {
172 ConfigError::CredentialError(format!("Failed to resolve API key: {}", e))
173 })?;
174 let resolved_secret = store
175 .get_credential(api_secret, Some("REDIS_CLOUD_API_SECRET"))
176 .map_err(|e| {
177 ConfigError::CredentialError(format!("Failed to resolve API secret: {}", e))
178 })?;
179 let resolved_url = store
180 .get_credential(api_url, Some("REDIS_CLOUD_API_URL"))
181 .map_err(|e| {
182 ConfigError::CredentialError(format!("Failed to resolve API URL: {}", e))
183 })?;
184
185 Ok(Some((resolved_key, resolved_secret, resolved_url)))
186 }
187 _ => Ok(None),
188 }
189 }
190
191 #[allow(clippy::type_complexity)]
193 pub fn resolve_enterprise_credentials(
194 &self,
195 ) -> Result<Option<(String, String, Option<String>, bool)>> {
196 match &self.credentials {
197 ProfileCredentials::Enterprise {
198 url,
199 username,
200 password,
201 insecure,
202 } => {
203 let store = CredentialStore::new();
204
205 let resolved_url = store
207 .get_credential(url, Some("REDIS_ENTERPRISE_URL"))
208 .map_err(|e| {
209 ConfigError::CredentialError(format!("Failed to resolve URL: {}", e))
210 })?;
211 let resolved_username = store
212 .get_credential(username, Some("REDIS_ENTERPRISE_USER"))
213 .map_err(|e| {
214 ConfigError::CredentialError(format!("Failed to resolve username: {}", e))
215 })?;
216 let resolved_password = password
217 .as_ref()
218 .map(|p| {
219 store
220 .get_credential(p, Some("REDIS_ENTERPRISE_PASSWORD"))
221 .map_err(|e| {
222 ConfigError::CredentialError(format!(
223 "Failed to resolve password: {}",
224 e
225 ))
226 })
227 })
228 .transpose()?;
229
230 Ok(Some((
231 resolved_url,
232 resolved_username,
233 resolved_password,
234 *insecure,
235 )))
236 }
237 _ => Ok(None),
238 }
239 }
240
241 #[allow(clippy::type_complexity)]
243 pub fn database_credentials(&self) -> Option<(&str, u16, Option<&str>, bool, &str, u8)> {
244 match &self.credentials {
245 ProfileCredentials::Database {
246 host,
247 port,
248 password,
249 tls,
250 username,
251 database,
252 } => Some((
253 host.as_str(),
254 *port,
255 password.as_deref(),
256 *tls,
257 username.as_str(),
258 *database,
259 )),
260 _ => None,
261 }
262 }
263
264 #[allow(clippy::type_complexity)]
266 pub fn resolve_database_credentials(
267 &self,
268 ) -> Result<Option<(String, u16, Option<String>, bool, String, u8)>> {
269 match &self.credentials {
270 ProfileCredentials::Database {
271 host,
272 port,
273 password,
274 tls,
275 username,
276 database,
277 } => {
278 let store = CredentialStore::new();
279
280 let resolved_host =
282 store
283 .get_credential(host, Some("REDIS_HOST"))
284 .map_err(|e| {
285 ConfigError::CredentialError(format!("Failed to resolve host: {}", e))
286 })?;
287 let resolved_username = store
288 .get_credential(username, Some("REDIS_USERNAME"))
289 .map_err(|e| {
290 ConfigError::CredentialError(format!("Failed to resolve username: {}", e))
291 })?;
292 let resolved_password = password
293 .as_ref()
294 .map(|p| {
295 store
296 .get_credential(p, Some("REDIS_PASSWORD"))
297 .map_err(|e| {
298 ConfigError::CredentialError(format!(
299 "Failed to resolve password: {}",
300 e
301 ))
302 })
303 })
304 .transpose()?;
305
306 Ok(Some((
307 resolved_host,
308 *port,
309 resolved_password,
310 *tls,
311 resolved_username,
312 *database,
313 )))
314 }
315 _ => Ok(None),
316 }
317 }
318}
319
320impl Config {
321 pub fn find_first_profile_of_type(&self, deployment_type: DeploymentType) -> Option<&str> {
323 let mut profiles: Vec<_> = self
324 .profiles
325 .iter()
326 .filter(|(_, p)| p.deployment_type == deployment_type)
327 .map(|(name, _)| name.as_str())
328 .collect();
329 profiles.sort();
330 profiles.first().copied()
331 }
332
333 pub fn get_profiles_of_type(&self, deployment_type: DeploymentType) -> Vec<&str> {
335 let mut profiles: Vec<_> = self
336 .profiles
337 .iter()
338 .filter(|(_, p)| p.deployment_type == deployment_type)
339 .map(|(name, _)| name.as_str())
340 .collect();
341 profiles.sort();
342 profiles
343 }
344
345 pub fn resolve_enterprise_profile(&self, explicit_profile: Option<&str>) -> Result<String> {
347 if let Some(profile_name) = explicit_profile {
348 return Ok(profile_name.to_string());
350 }
351
352 if let Some(ref default) = self.default_enterprise {
353 return Ok(default.clone());
355 }
356
357 if let Some(profile_name) = self.find_first_profile_of_type(DeploymentType::Enterprise) {
358 return Ok(profile_name.to_string());
360 }
361
362 let cloud_profiles = self.get_profiles_of_type(DeploymentType::Cloud);
364 let database_profiles = self.get_profiles_of_type(DeploymentType::Database);
365 if !cloud_profiles.is_empty() || !database_profiles.is_empty() {
366 let mut suggestions = Vec::new();
367 if !cloud_profiles.is_empty() {
368 suggestions.push(format!("cloud: {}", cloud_profiles.join(", ")));
369 }
370 if !database_profiles.is_empty() {
371 suggestions.push(format!("database: {}", database_profiles.join(", ")));
372 }
373 Err(ConfigError::NoProfilesOfType {
374 deployment_type: "enterprise".to_string(),
375 suggestion: format!(
376 "Available profiles: {}. Use 'redisctl profile set' to create an enterprise profile.",
377 suggestions.join("; ")
378 ),
379 })
380 } else {
381 Err(ConfigError::NoProfilesOfType {
382 deployment_type: "enterprise".to_string(),
383 suggestion: "Use 'redisctl profile set' to create a profile.".to_string(),
384 })
385 }
386 }
387
388 pub fn resolve_cloud_profile(&self, explicit_profile: Option<&str>) -> Result<String> {
390 if let Some(profile_name) = explicit_profile {
391 return Ok(profile_name.to_string());
393 }
394
395 if let Some(ref default) = self.default_cloud {
396 return Ok(default.clone());
398 }
399
400 if let Some(profile_name) = self.find_first_profile_of_type(DeploymentType::Cloud) {
401 return Ok(profile_name.to_string());
403 }
404
405 let enterprise_profiles = self.get_profiles_of_type(DeploymentType::Enterprise);
407 let database_profiles = self.get_profiles_of_type(DeploymentType::Database);
408 if !enterprise_profiles.is_empty() || !database_profiles.is_empty() {
409 let mut suggestions = Vec::new();
410 if !enterprise_profiles.is_empty() {
411 suggestions.push(format!("enterprise: {}", enterprise_profiles.join(", ")));
412 }
413 if !database_profiles.is_empty() {
414 suggestions.push(format!("database: {}", database_profiles.join(", ")));
415 }
416 Err(ConfigError::NoProfilesOfType {
417 deployment_type: "cloud".to_string(),
418 suggestion: format!(
419 "Available profiles: {}. Use 'redisctl profile set' to create a cloud profile.",
420 suggestions.join("; ")
421 ),
422 })
423 } else {
424 Err(ConfigError::NoProfilesOfType {
425 deployment_type: "cloud".to_string(),
426 suggestion: "Use 'redisctl profile set' to create a profile.".to_string(),
427 })
428 }
429 }
430
431 pub fn resolve_database_profile(&self, explicit_profile: Option<&str>) -> Result<String> {
433 if let Some(profile_name) = explicit_profile {
434 return Ok(profile_name.to_string());
436 }
437
438 if let Some(ref default) = self.default_database {
439 return Ok(default.clone());
441 }
442
443 if let Some(profile_name) = self.find_first_profile_of_type(DeploymentType::Database) {
444 return Ok(profile_name.to_string());
446 }
447
448 let cloud_profiles = self.get_profiles_of_type(DeploymentType::Cloud);
450 let enterprise_profiles = self.get_profiles_of_type(DeploymentType::Enterprise);
451 if !cloud_profiles.is_empty() || !enterprise_profiles.is_empty() {
452 let mut suggestions = Vec::new();
453 if !cloud_profiles.is_empty() {
454 suggestions.push(format!("cloud: {}", cloud_profiles.join(", ")));
455 }
456 if !enterprise_profiles.is_empty() {
457 suggestions.push(format!("enterprise: {}", enterprise_profiles.join(", ")));
458 }
459 Err(ConfigError::NoProfilesOfType {
460 deployment_type: "database".to_string(),
461 suggestion: format!(
462 "Available profiles: {}. Use 'redisctl profile set' to create a database profile.",
463 suggestions.join("; ")
464 ),
465 })
466 } else {
467 Err(ConfigError::NoProfilesOfType {
468 deployment_type: "database".to_string(),
469 suggestion: "Use 'redisctl profile set' to create a profile.".to_string(),
470 })
471 }
472 }
473
474 pub fn load() -> Result<Self> {
476 let config_path = Self::config_path()?;
477 Self::load_from_path(&config_path)
478 }
479
480 pub fn load_from_path(config_path: &Path) -> Result<Self> {
482 if !config_path.exists() {
483 return Ok(Config::default());
484 }
485
486 let content = fs::read_to_string(config_path).map_err(|e| ConfigError::LoadError {
487 path: config_path.display().to_string(),
488 source: e,
489 })?;
490
491 let expanded_content = Self::expand_env_vars(&content);
493
494 let config: Config = toml::from_str(&expanded_content)?;
495
496 Ok(config)
497 }
498
499 pub fn save(&self) -> Result<()> {
501 let config_path = Self::config_path()?;
502 self.save_to_path(&config_path)
503 }
504
505 pub fn save_to_path(&self, config_path: &Path) -> Result<()> {
507 if let Some(parent) = config_path.parent() {
509 fs::create_dir_all(parent).map_err(|e| ConfigError::SaveError {
510 path: parent.display().to_string(),
511 source: e,
512 })?;
513 }
514
515 let content = toml::to_string_pretty(self)?;
516
517 fs::write(config_path, content).map_err(|e| ConfigError::SaveError {
518 path: config_path.display().to_string(),
519 source: e,
520 })?;
521
522 Ok(())
523 }
524
525 pub fn set_profile(&mut self, name: String, profile: Profile) {
527 self.profiles.insert(name, profile);
528 }
529
530 pub fn remove_profile(&mut self, name: &str) -> Option<Profile> {
532 if self.default_enterprise.as_deref() == Some(name) {
534 self.default_enterprise = None;
535 }
536 if self.default_cloud.as_deref() == Some(name) {
537 self.default_cloud = None;
538 }
539 if self.default_database.as_deref() == Some(name) {
540 self.default_database = None;
541 }
542 self.profiles.remove(name)
543 }
544
545 pub fn list_profiles(&self) -> Vec<(&String, &Profile)> {
547 let mut profiles: Vec<_> = self.profiles.iter().collect();
548 profiles.sort_by_key(|(name, _)| *name);
549 profiles
550 }
551
552 pub fn config_path() -> Result<PathBuf> {
561 #[cfg(target_os = "macos")]
563 {
564 if let Some(base_dirs) = BaseDirs::new() {
565 let home_dir = base_dirs.home_dir();
566 let linux_style_path = home_dir
567 .join(".config")
568 .join("redisctl")
569 .join("config.toml");
570
571 if linux_style_path.exists() {
573 return Ok(linux_style_path);
574 }
575
576 if linux_style_path
578 .parent()
579 .map(|p| p.exists())
580 .unwrap_or(false)
581 {
582 return Ok(linux_style_path);
583 }
584 }
585 }
586
587 let proj_dirs =
589 ProjectDirs::from("com", "redis", "redisctl").ok_or(ConfigError::ConfigDirError)?;
590
591 Ok(proj_dirs.config_dir().join("config.toml"))
592 }
593
594 fn expand_env_vars(content: &str) -> String {
606 let expanded =
609 shellexpand::env_with_context_no_errors(content, |var| std::env::var(var).ok());
610 expanded.to_string()
611 }
612}
613
614fn default_cloud_url() -> String {
615 "https://api.redislabs.com/v1".to_string()
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn test_config_serialization() {
624 let mut config = Config::default();
625
626 let cloud_profile = Profile {
627 deployment_type: DeploymentType::Cloud,
628 credentials: ProfileCredentials::Cloud {
629 api_key: "test-key".to_string(),
630 api_secret: "test-secret".to_string(),
631 api_url: "https://api.redislabs.com/v1".to_string(),
632 },
633 files_api_key: None,
634 resilience: None,
635 };
636
637 config.set_profile("test".to_string(), cloud_profile);
638 config.default_cloud = Some("test".to_string());
639
640 let serialized = toml::to_string(&config).unwrap();
641 let deserialized: Config = toml::from_str(&serialized).unwrap();
642
643 assert_eq!(config.default_cloud, deserialized.default_cloud);
644 assert_eq!(config.profiles.len(), deserialized.profiles.len());
645 }
646
647 #[test]
648 fn test_profile_credential_access() {
649 let cloud_profile = Profile {
650 deployment_type: DeploymentType::Cloud,
651 credentials: ProfileCredentials::Cloud {
652 api_key: "key".to_string(),
653 api_secret: "secret".to_string(),
654 api_url: "url".to_string(),
655 },
656 files_api_key: None,
657 resilience: None,
658 };
659
660 let (key, secret, url) = cloud_profile.cloud_credentials().unwrap();
661 assert_eq!(key, "key");
662 assert_eq!(secret, "secret");
663 assert_eq!(url, "url");
664 assert!(cloud_profile.enterprise_credentials().is_none());
665 }
666
667 #[test]
668 #[serial_test::serial]
669 fn test_env_var_expansion() {
670 unsafe {
672 std::env::set_var("TEST_API_KEY", "test-key-value");
673 std::env::set_var("TEST_API_SECRET", "test-secret-value");
674 }
675
676 let content = r#"
677[profiles.test]
678deployment_type = "cloud"
679api_key = "${TEST_API_KEY}"
680api_secret = "${TEST_API_SECRET}"
681"#;
682
683 let expanded = Config::expand_env_vars(content);
684 assert!(expanded.contains("test-key-value"));
685 assert!(expanded.contains("test-secret-value"));
686
687 unsafe {
689 std::env::remove_var("TEST_API_KEY");
690 std::env::remove_var("TEST_API_SECRET");
691 }
692 }
693
694 #[test]
695 #[serial_test::serial]
696 fn test_env_var_expansion_with_defaults() {
697 unsafe {
699 std::env::remove_var("NONEXISTENT_VAR"); }
701
702 let content = r#"
703[profiles.test]
704deployment_type = "cloud"
705api_key = "${NONEXISTENT_VAR:-default-key}"
706api_url = "${NONEXISTENT_URL:-https://api.redislabs.com/v1}"
707"#;
708
709 let expanded = Config::expand_env_vars(content);
710 assert!(expanded.contains("default-key"));
711 assert!(expanded.contains("https://api.redislabs.com/v1"));
712 }
713
714 #[test]
715 #[serial_test::serial]
716 fn test_env_var_expansion_mixed() {
717 unsafe {
719 std::env::set_var("TEST_DYNAMIC_KEY", "dynamic-value");
720 }
721
722 let content = r#"
723[profiles.test]
724deployment_type = "cloud"
725api_key = "${TEST_DYNAMIC_KEY}"
726api_secret = "static-secret"
727api_url = "${MISSING_VAR:-https://api.redislabs.com/v1}"
728"#;
729
730 let expanded = Config::expand_env_vars(content);
731 assert!(expanded.contains("dynamic-value"));
732 assert!(expanded.contains("static-secret"));
733 assert!(expanded.contains("https://api.redislabs.com/v1"));
734
735 unsafe {
737 std::env::remove_var("TEST_DYNAMIC_KEY");
738 }
739 }
740
741 #[test]
742 #[serial_test::serial]
743 fn test_full_config_with_env_expansion() {
744 unsafe {
746 std::env::set_var("REDIS_TEST_KEY", "expanded-key");
747 std::env::set_var("REDIS_TEST_SECRET", "expanded-secret");
748 }
749
750 let config_content = r#"
751default_cloud = "test"
752
753[profiles.test]
754deployment_type = "cloud"
755api_key = "${REDIS_TEST_KEY}"
756api_secret = "${REDIS_TEST_SECRET}"
757api_url = "${REDIS_TEST_URL:-https://api.redislabs.com/v1}"
758"#;
759
760 let expanded = Config::expand_env_vars(config_content);
761 let config: Config = toml::from_str(&expanded).unwrap();
762
763 assert_eq!(config.default_cloud, Some("test".to_string()));
764
765 let profile = config.profiles.get("test").unwrap();
766 let (key, secret, url) = profile.cloud_credentials().unwrap();
767 assert_eq!(key, "expanded-key");
768 assert_eq!(secret, "expanded-secret");
769 assert_eq!(url, "https://api.redislabs.com/v1");
770
771 unsafe {
773 std::env::remove_var("REDIS_TEST_KEY");
774 std::env::remove_var("REDIS_TEST_SECRET");
775 }
776 }
777
778 #[test]
779 fn test_enterprise_profile_resolution() {
780 let mut config = Config::default();
781
782 let enterprise_profile = Profile {
784 deployment_type: DeploymentType::Enterprise,
785 credentials: ProfileCredentials::Enterprise {
786 url: "https://localhost:9443".to_string(),
787 username: "admin".to_string(),
788 password: Some("password".to_string()),
789 insecure: false,
790 },
791 files_api_key: None,
792 resilience: None,
793 };
794 config.set_profile("ent1".to_string(), enterprise_profile);
795
796 assert_eq!(
798 config.resolve_enterprise_profile(Some("ent1")).unwrap(),
799 "ent1"
800 );
801
802 assert_eq!(config.resolve_enterprise_profile(None).unwrap(), "ent1");
804
805 config.default_enterprise = Some("ent1".to_string());
807 assert_eq!(config.resolve_enterprise_profile(None).unwrap(), "ent1");
808 }
809
810 #[test]
811 fn test_cloud_profile_resolution() {
812 let mut config = Config::default();
813
814 let cloud_profile = Profile {
816 deployment_type: DeploymentType::Cloud,
817 credentials: ProfileCredentials::Cloud {
818 api_key: "key".to_string(),
819 api_secret: "secret".to_string(),
820 api_url: "https://api.redislabs.com/v1".to_string(),
821 },
822 files_api_key: None,
823 resilience: None,
824 };
825 config.set_profile("cloud1".to_string(), cloud_profile);
826
827 assert_eq!(
829 config.resolve_cloud_profile(Some("cloud1")).unwrap(),
830 "cloud1"
831 );
832
833 assert_eq!(config.resolve_cloud_profile(None).unwrap(), "cloud1");
835
836 config.default_cloud = Some("cloud1".to_string());
838 assert_eq!(config.resolve_cloud_profile(None).unwrap(), "cloud1");
839 }
840
841 #[test]
842 fn test_mixed_profile_resolution() {
843 let mut config = Config::default();
844
845 let cloud_profile = Profile {
847 deployment_type: DeploymentType::Cloud,
848 credentials: ProfileCredentials::Cloud {
849 api_key: "key".to_string(),
850 api_secret: "secret".to_string(),
851 api_url: "https://api.redislabs.com/v1".to_string(),
852 },
853 files_api_key: None,
854 resilience: None,
855 };
856 config.set_profile("cloud1".to_string(), cloud_profile.clone());
857 config.set_profile("cloud2".to_string(), cloud_profile);
858
859 let enterprise_profile = Profile {
861 deployment_type: DeploymentType::Enterprise,
862 credentials: ProfileCredentials::Enterprise {
863 url: "https://localhost:9443".to_string(),
864 username: "admin".to_string(),
865 password: Some("password".to_string()),
866 insecure: false,
867 },
868 files_api_key: None,
869 resilience: None,
870 };
871 config.set_profile("ent1".to_string(), enterprise_profile.clone());
872 config.set_profile("ent2".to_string(), enterprise_profile);
873
874 assert_eq!(config.resolve_cloud_profile(None).unwrap(), "cloud1");
876 assert_eq!(config.resolve_enterprise_profile(None).unwrap(), "ent1");
877
878 config.default_cloud = Some("cloud2".to_string());
880 config.default_enterprise = Some("ent2".to_string());
881
882 assert_eq!(config.resolve_cloud_profile(None).unwrap(), "cloud2");
884 assert_eq!(config.resolve_enterprise_profile(None).unwrap(), "ent2");
885 }
886
887 #[test]
888 fn test_no_profile_errors() {
889 let config = Config::default();
890
891 assert!(config.resolve_enterprise_profile(None).is_err());
893 assert!(config.resolve_cloud_profile(None).is_err());
894 }
895
896 #[test]
897 fn test_wrong_profile_type_help() {
898 let mut config = Config::default();
899
900 let cloud_profile = Profile {
902 deployment_type: DeploymentType::Cloud,
903 credentials: ProfileCredentials::Cloud {
904 api_key: "key".to_string(),
905 api_secret: "secret".to_string(),
906 api_url: "https://api.redislabs.com/v1".to_string(),
907 },
908 files_api_key: None,
909 resilience: None,
910 };
911 config.set_profile("cloud1".to_string(), cloud_profile);
912
913 let err = config.resolve_enterprise_profile(None).unwrap_err();
915 assert!(err.to_string().contains("No enterprise profiles"));
916 assert!(err.to_string().contains("cloud: cloud1"));
917 }
918
919 #[test]
920 fn test_database_profile_serialization() {
921 let mut config = Config::default();
922
923 let db_profile = Profile {
924 deployment_type: DeploymentType::Database,
925 credentials: ProfileCredentials::Database {
926 host: "localhost".to_string(),
927 port: 6379,
928 password: Some("secret".to_string()),
929 tls: true,
930 username: "default".to_string(),
931 database: 0,
932 },
933 files_api_key: None,
934 resilience: None,
935 };
936
937 config.set_profile("myredis".to_string(), db_profile);
938 config.default_database = Some("myredis".to_string());
939
940 let serialized = toml::to_string(&config).unwrap();
941 let deserialized: Config = toml::from_str(&serialized).unwrap();
942
943 assert_eq!(config.default_database, deserialized.default_database);
944 assert_eq!(config.profiles.len(), deserialized.profiles.len());
945
946 let profile = deserialized.profiles.get("myredis").unwrap();
947 assert_eq!(profile.deployment_type, DeploymentType::Database);
948
949 let (host, port, password, tls, username, database) =
950 profile.database_credentials().unwrap();
951 assert_eq!(host, "localhost");
952 assert_eq!(port, 6379);
953 assert_eq!(password, Some("secret"));
954 assert!(tls);
955 assert_eq!(username, "default");
956 assert_eq!(database, 0);
957 }
958
959 #[test]
960 fn test_database_profile_resolution() {
961 let mut config = Config::default();
962
963 let db_profile = Profile {
965 deployment_type: DeploymentType::Database,
966 credentials: ProfileCredentials::Database {
967 host: "localhost".to_string(),
968 port: 6379,
969 password: None,
970 tls: false,
971 username: "default".to_string(),
972 database: 0,
973 },
974 files_api_key: None,
975 resilience: None,
976 };
977 config.set_profile("db1".to_string(), db_profile);
978
979 assert_eq!(config.resolve_database_profile(Some("db1")).unwrap(), "db1");
981
982 assert_eq!(config.resolve_database_profile(None).unwrap(), "db1");
984
985 config.default_database = Some("db1".to_string());
987 assert_eq!(config.resolve_database_profile(None).unwrap(), "db1");
988 }
989
990 #[test]
991 fn test_database_profile_defaults() {
992 let toml_content = r#"
994[profiles.minimal]
995deployment_type = "database"
996host = "redis.example.com"
997port = 12345
998"#;
999 let config: Config = toml::from_str(toml_content).unwrap();
1000 let profile = config.profiles.get("minimal").unwrap();
1001
1002 let (host, port, password, tls, username, database) =
1003 profile.database_credentials().unwrap();
1004 assert_eq!(host, "redis.example.com");
1005 assert_eq!(port, 12345);
1006 assert!(password.is_none());
1007 assert!(tls); assert_eq!(username, "default"); assert_eq!(database, 0); }
1011}