zoom_cli/commands/
config.rs1use 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 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}