Skip to main content

zoom_cli/commands/
config.rs

1use crate::api::ApiError;
2use crate::config;
3use crate::output::{self, OutputConfig};
4
5pub fn show(profile_arg: Option<&str>, out: &OutputConfig) {
6    let summary = config::load_for_show(profile_arg);
7
8    if out.json {
9        let profiles_json: serde_json::Map<String, serde_json::Value> = summary
10            .profiles
11            .iter()
12            .map(|p| {
13                (
14                    p.name.clone(),
15                    serde_json::json!({
16                        "account_id": masked_or_unset(&p.account_id),
17                        "client_id": masked_or_unset(&p.client_id),
18                        "client_secret": masked_or_unset(&p.client_secret),
19                    }),
20                )
21            })
22            .collect();
23
24        let env_overrides_json: serde_json::Map<String, serde_json::Value> = summary
25            .env_overrides
26            .iter()
27            .map(|(k, v)| {
28                (
29                    k.to_string(),
30                    serde_json::Value::String(output::mask_credential(v)),
31                )
32            })
33            .collect();
34
35        out.print_data(
36            &serde_json::to_string_pretty(&serde_json::json!({
37                "config_file": summary.config_file.to_string_lossy(),
38                "file_exists": summary.file_exists,
39                "active_profile": summary.active_profile,
40                "profiles": profiles_json,
41                "env_overrides": env_overrides_json,
42            }))
43            .expect("serialize"),
44        );
45    } else {
46        out.print_data(&format!(
47            "Config file: {}{}",
48            summary.config_file.display(),
49            if summary.file_exists {
50                ""
51            } else {
52                "  (not found)"
53            },
54        ));
55
56        if summary.profiles.is_empty() && summary.env_overrides.is_empty() {
57            out.print_data("\nNo configuration found. Run `zoom init` to set up credentials.");
58            return;
59        }
60
61        for profile in &summary.profiles {
62            let active_marker = if profile.name == summary.active_profile {
63                "  (active)"
64            } else {
65                ""
66            };
67            out.print_data(&format!("\nProfile: {}{}", profile.name, active_marker));
68            out.print_data(&format!(
69                "  account_id:    {}",
70                masked_or_unset(&profile.account_id)
71            ));
72            out.print_data(&format!(
73                "  client_id:     {}",
74                masked_or_unset(&profile.client_id)
75            ));
76            out.print_data(&format!(
77                "  client_secret: {}",
78                masked_or_unset(&profile.client_secret)
79            ));
80        }
81
82        if !summary.env_overrides.is_empty() {
83            out.print_data("\nEnvironment overrides (take precedence over file):");
84            for (var, val) in &summary.env_overrides {
85                out.print_data(&format!("  {}={}", var, output::mask_credential(val)));
86            }
87        }
88    }
89}
90
91pub fn delete(profile_name: &str, force: bool, out: &OutputConfig) -> Result<(), ApiError> {
92    let config_path = config::config_path();
93
94    if !force {
95        eprint!("Delete profile '{profile_name}'? [y/N] ");
96        use std::io::{BufRead, IsTerminal};
97        if !std::io::stdin().is_terminal() {
98            eprintln!();
99            eprintln!("Use --force to delete non-interactively.");
100            return Err(ApiError::Other(
101                "stdin is not a terminal; use --force to delete non-interactively".into(),
102            ));
103        }
104        let mut input = String::new();
105        std::io::stdin().lock().read_line(&mut input).unwrap_or(0);
106        if input.trim().to_lowercase() != "y" {
107            out.print_message("Aborted.");
108            return Ok(());
109        }
110    }
111
112    config::delete_profile(&config_path, profile_name)?;
113    out.print_message(&format!("Profile '{profile_name}' deleted."));
114    Ok(())
115}
116
117fn masked_or_unset(value: &Option<String>) -> String {
118    match value {
119        Some(v) => output::mask_credential(v),
120        None => "(not set)".to_owned(),
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::test_support::{EnvVarGuard, ProcessEnvLock, set_config_dir_env, write_config};
128    use tempfile::TempDir;
129
130    #[test]
131    fn show_json_includes_active_profile_and_masked_credentials() {
132        let _lock = ProcessEnvLock::acquire().unwrap();
133        let dir = TempDir::new().unwrap();
134        write_config(
135            dir.path(),
136            r#"
137[default]
138account_id = "acct-long-enough-123"
139client_id = "cid-long-enough-456"
140client_secret = "csec-long-enough-789"
141"#,
142        )
143        .unwrap();
144
145        let _cfg_dir = set_config_dir_env(dir.path());
146        let _acct = EnvVarGuard::unset("ZOOM_ACCOUNT_ID");
147        let _cid = EnvVarGuard::unset("ZOOM_CLIENT_ID");
148        let _csec = EnvVarGuard::unset("ZOOM_CLIENT_SECRET");
149        let _prof = EnvVarGuard::unset("ZOOM_PROFILE");
150
151        // Capture stdout by redirecting through a buffer isn't straightforward;
152        // instead call load_for_show directly and validate the summary.
153        let summary = config::load_for_show(None);
154        assert_eq!(summary.active_profile, "default");
155        assert!(summary.file_exists);
156        assert_eq!(summary.profiles.len(), 1);
157        assert_eq!(summary.profiles[0].name, "default");
158        assert_eq!(
159            summary.profiles[0].account_id.as_deref(),
160            Some("acct-long-enough-123")
161        );
162        assert!(summary.env_overrides.is_empty());
163    }
164
165    #[test]
166    fn show_lists_all_profiles_with_default_first() {
167        let _lock = ProcessEnvLock::acquire().unwrap();
168        let dir = TempDir::new().unwrap();
169        write_config(
170            dir.path(),
171            r#"
172[default]
173account_id = "def-acct"
174client_id = "def-cid"
175client_secret = "def-csec"
176
177[work]
178account_id = "work-acct"
179client_id = "work-cid"
180client_secret = "work-csec"
181"#,
182        )
183        .unwrap();
184
185        let _cfg_dir = set_config_dir_env(dir.path());
186        let _env_acct = EnvVarGuard::unset("ZOOM_ACCOUNT_ID");
187        let _env_cid = EnvVarGuard::unset("ZOOM_CLIENT_ID");
188        let _env_csec = EnvVarGuard::unset("ZOOM_CLIENT_SECRET");
189        let _env_prof = EnvVarGuard::unset("ZOOM_PROFILE");
190
191        let summary = config::load_for_show(None);
192        assert_eq!(summary.profiles.len(), 2);
193        assert_eq!(summary.profiles[0].name, "default");
194        assert_eq!(summary.profiles[1].name, "work");
195    }
196
197    #[test]
198    fn show_marks_profile_arg_as_active() {
199        let _lock = ProcessEnvLock::acquire().unwrap();
200        let dir = TempDir::new().unwrap();
201        write_config(
202            dir.path(),
203            r#"
204[default]
205account_id = "def-acct"
206client_id = "def-cid"
207client_secret = "def-csec"
208
209[work]
210account_id = "work-acct"
211client_id = "work-cid"
212client_secret = "work-csec"
213"#,
214        )
215        .unwrap();
216
217        let _cfg_dir = set_config_dir_env(dir.path());
218        let _env_acct = EnvVarGuard::unset("ZOOM_ACCOUNT_ID");
219        let _env_cid = EnvVarGuard::unset("ZOOM_CLIENT_ID");
220        let _env_csec = EnvVarGuard::unset("ZOOM_CLIENT_SECRET");
221        let _env_prof = EnvVarGuard::unset("ZOOM_PROFILE");
222
223        let summary = config::load_for_show(Some("work"));
224        assert_eq!(summary.active_profile, "work");
225    }
226
227    #[test]
228    fn show_reports_env_overrides() {
229        let _lock = ProcessEnvLock::acquire().unwrap();
230        let dir = TempDir::new().unwrap();
231        let _cfg_dir = set_config_dir_env(dir.path());
232        let _acct = EnvVarGuard::set("ZOOM_ACCOUNT_ID", "env-account-id-long");
233        let _cid = EnvVarGuard::unset("ZOOM_CLIENT_ID");
234        let _csec = EnvVarGuard::unset("ZOOM_CLIENT_SECRET");
235        let _prof = EnvVarGuard::unset("ZOOM_PROFILE");
236
237        let summary = config::load_for_show(None);
238        assert_eq!(summary.env_overrides.len(), 1);
239        assert_eq!(summary.env_overrides[0].0, "ZOOM_ACCOUNT_ID");
240        assert_eq!(summary.env_overrides[0].1, "env-account-id-long");
241    }
242
243    #[test]
244    fn show_handles_missing_config_file() {
245        let _lock = ProcessEnvLock::acquire().unwrap();
246        let dir = TempDir::new().unwrap();
247        let _cfg_dir = set_config_dir_env(dir.path());
248        let _acct = EnvVarGuard::unset("ZOOM_ACCOUNT_ID");
249        let _cid = EnvVarGuard::unset("ZOOM_CLIENT_ID");
250        let _csec = EnvVarGuard::unset("ZOOM_CLIENT_SECRET");
251        let _prof = EnvVarGuard::unset("ZOOM_PROFILE");
252
253        let summary = config::load_for_show(None);
254        assert!(!summary.file_exists);
255        assert!(summary.profiles.is_empty());
256    }
257
258    #[test]
259    fn masked_or_unset_masks_present_values() {
260        assert_eq!(
261            masked_or_unset(&Some("abcdefghijklmnop".to_owned())),
262            "abcdef…mnop"
263        );
264        assert_eq!(masked_or_unset(&None), "(not set)");
265    }
266
267    #[test]
268    fn delete_removes_profile_from_config() {
269        let _lock = ProcessEnvLock::acquire().unwrap();
270        let dir = TempDir::new().unwrap();
271        write_config(
272            dir.path(),
273            r#"
274[default]
275account_id = "def-acct"
276client_id = "def-cid"
277client_secret = "def-csec"
278
279[work]
280account_id = "work-acct"
281client_id = "work-cid"
282client_secret = "work-csec"
283"#,
284        )
285        .unwrap();
286
287        let _cfg_dir = set_config_dir_env(dir.path());
288        let _env_acct = EnvVarGuard::unset("ZOOM_ACCOUNT_ID");
289        let _env_cid = EnvVarGuard::unset("ZOOM_CLIENT_ID");
290        let _env_csec = EnvVarGuard::unset("ZOOM_CLIENT_SECRET");
291        let _env_prof = EnvVarGuard::unset("ZOOM_PROFILE");
292        let path = dir.path().join("zoom-cli").join("config.toml");
293        config::delete_profile(&path, "work").unwrap();
294
295        let summary = config::load_for_show(None);
296        assert_eq!(summary.profiles.len(), 1);
297        assert_eq!(summary.profiles[0].name, "default");
298    }
299
300    #[test]
301    fn delete_returns_not_found_for_missing_profile() {
302        let _lock = ProcessEnvLock::acquire().unwrap();
303        let dir = TempDir::new().unwrap();
304        write_config(
305            dir.path(),
306            "[default]\naccount_id=\"x\"\nclient_id=\"y\"\nclient_secret=\"z\"\n",
307        )
308        .unwrap();
309
310        let path = dir.path().join("zoom-cli").join("config.toml");
311        let err = config::delete_profile(&path, "nonexistent").unwrap_err();
312        assert!(matches!(err, crate::api::ApiError::NotFound(_)));
313    }
314}