1use anyhow::Result;
21use serde::Serialize;
22
23use crate::drive::auth::{DRIVE_CLIENT_ID, DRIVE_CLIENT_SECRET, DRIVE_REFRESH_TOKEN};
24use crate::utils::env::EnvSource;
25use crate::utils::secret_env::secret_var_is_set;
26use crate::utils::settings::DriveSettings;
27
28pub const DRIVE_ACCOUNT_ENV: &str = "OMNI_DEV_DRIVE_ACCOUNT";
32
33pub fn active_drive_account_from<E: EnvSource>(raw: &E) -> Option<String> {
40 raw.var(DRIVE_ACCOUNT_ENV).filter(|s| !s.is_empty())
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum ResolvedAccount {
47 Named(String),
49 Unconfigured,
55}
56
57pub fn resolve_account<E: EnvSource>(
83 raw: &E,
84 settings: &DriveSettings,
85 explicit: Option<&str>,
86) -> Result<ResolvedAccount> {
87 if has_literal_credentials(raw) {
88 return Ok(ResolvedAccount::Unconfigured);
89 }
90 if let Some(name) = explicit {
91 validate_account(settings, name)?;
92 return Ok(ResolvedAccount::Named(name.to_string()));
93 }
94 if settings.accounts.is_empty() {
95 return Ok(ResolvedAccount::Unconfigured);
96 }
97 resolve_default_account(settings).map(ResolvedAccount::Named)
98}
99
100pub fn resolve_account_for_write<E: EnvSource>(
112 raw: &E,
113 settings: &DriveSettings,
114 explicit: Option<&str>,
115) -> Result<ResolvedAccount> {
116 if has_literal_credentials(raw) {
117 return Ok(ResolvedAccount::Unconfigured);
118 }
119 if let Some(name) = explicit {
120 return Ok(ResolvedAccount::Named(name.to_string()));
121 }
122 if settings.accounts.is_empty() {
123 return Ok(ResolvedAccount::Unconfigured);
124 }
125 resolve_default_account(settings).map(ResolvedAccount::Named)
126}
127
128fn has_literal_credentials<E: EnvSource>(raw: &E) -> bool {
131 raw.var(DRIVE_CLIENT_ID).is_some()
132 && secret_var_is_set(raw, DRIVE_CLIENT_SECRET)
133 && secret_var_is_set(raw, DRIVE_REFRESH_TOKEN)
134}
135
136pub fn validate_account(settings: &DriveSettings, name: &str) -> Result<()> {
144 if settings.accounts.contains_key(name) {
145 return Ok(());
146 }
147 Err(anyhow::anyhow!(
148 "unknown Drive account '{name}'; known accounts: {}",
149 known_accounts(settings)
150 ))
151}
152
153fn known_accounts(settings: &DriveSettings) -> String {
156 if settings.accounts.is_empty() {
157 "(none)".to_string()
158 } else {
159 let mut names: Vec<&str> = settings.accounts.keys().map(String::as_str).collect();
160 names.sort_unstable();
161 names.join(", ")
162 }
163}
164
165fn resolve_default_account(settings: &DriveSettings) -> Result<String> {
169 if let Some(default) = &settings.default_account {
170 return if settings.accounts.contains_key(default) {
171 Ok(default.clone())
172 } else {
173 Err(anyhow::anyhow!(
174 "configured default Drive account '{default}' no longer exists; known accounts: {}",
175 known_accounts(settings)
176 ))
177 };
178 }
179 let mut names = settings.accounts.keys();
180 if let (Some(sole), None) = (names.next(), names.next()) {
181 return Ok(sole.clone());
182 }
183 Err(anyhow::anyhow!(
184 "multiple Drive accounts configured and no default set; pass --account or run \
185 `drive account set-default <name>`"
186 ))
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
192pub struct AccountSummary {
193 pub name: String,
195 pub email_address: Option<String>,
197 pub scope: Option<String>,
199 pub is_default: bool,
201}
202
203pub fn list_accounts(settings: &DriveSettings) -> Vec<AccountSummary> {
205 let mut names: Vec<&String> = settings.accounts.keys().collect();
206 names.sort();
207 names
208 .into_iter()
209 .map(|name| {
210 let account = &settings.accounts[name];
211 AccountSummary {
212 name: name.clone(),
213 email_address: account.email_address.clone(),
214 scope: account.scope.clone(),
215 is_default: settings.default_account.as_deref() == Some(name.as_str()),
216 }
217 })
218 .collect()
219}
220
221#[cfg(test)]
222#[allow(clippy::unwrap_used)]
223mod tests {
224 use super::*;
225 use crate::test_support::env::MapEnv;
226 use crate::utils::settings::DriveAccountSettings;
227 use std::collections::HashMap;
228
229 fn account(email: Option<&str>, scope: Option<&str>) -> DriveAccountSettings {
230 DriveAccountSettings {
231 client_id: Some("id".to_string()),
232 client_secret: Some("secret".to_string()),
233 client_secret_file: None,
234 refresh_token: Some("token".to_string()),
235 refresh_token_file: None,
236 scope: scope.map(str::to_string),
237 email_address: email.map(str::to_string),
238 chrome_profile_from_email: false,
239 browser_command: None,
240 lease_backup_folder_id: None,
241 write_permissions: crate::utils::settings::WritePermissionsSettings::default(),
242 }
243 }
244
245 fn settings_with(
246 default_account: Option<&str>,
247 accounts: &[(&str, DriveAccountSettings)],
248 ) -> DriveSettings {
249 DriveSettings {
250 default_account: default_account.map(str::to_string),
251 accounts: accounts
252 .iter()
253 .map(|(name, acc)| {
254 (
255 (*name).to_string(),
256 DriveAccountSettings {
257 client_id: acc.client_id.clone(),
258 client_secret: acc.client_secret.clone(),
259 client_secret_file: acc.client_secret_file.clone(),
260 refresh_token: acc.refresh_token.clone(),
261 refresh_token_file: acc.refresh_token_file.clone(),
262 scope: acc.scope.clone(),
263 email_address: acc.email_address.clone(),
264 chrome_profile_from_email: acc.chrome_profile_from_email,
265 browser_command: acc.browser_command.clone(),
266 lease_backup_folder_id: acc.lease_backup_folder_id.clone(),
267 write_permissions:
268 crate::utils::settings::WritePermissionsSettings::default(),
269 },
270 )
271 })
272 .collect::<HashMap<_, _>>(),
273 }
274 }
275
276 #[test]
277 fn active_drive_account_from_reads_and_trims_empty() {
278 assert_eq!(
279 active_drive_account_from(&MapEnv::new().with(DRIVE_ACCOUNT_ENV, "work")),
280 Some("work".to_string())
281 );
282 assert_eq!(
283 active_drive_account_from(&MapEnv::new().with(DRIVE_ACCOUNT_ENV, "")),
284 None
285 );
286 assert_eq!(active_drive_account_from(&MapEnv::new()), None);
287 }
288
289 #[test]
290 fn resolve_literal_env_bypasses_account_resolution() {
291 let raw = MapEnv::new()
292 .with(DRIVE_CLIENT_ID, "id")
293 .with(DRIVE_CLIENT_SECRET, "secret")
294 .with(DRIVE_REFRESH_TOKEN, "token");
295 let settings = settings_with(
298 None,
299 &[("a", account(None, None)), ("b", account(None, None))],
300 );
301 assert_eq!(
302 resolve_account(&raw, &settings, Some("a")).unwrap(),
303 ResolvedAccount::Unconfigured
304 );
305 assert_eq!(
306 resolve_account(&raw, &settings, None).unwrap(),
307 ResolvedAccount::Unconfigured
308 );
309 }
310
311 #[test]
312 fn resolve_literal_env_requires_all_three_keys() {
313 let raw = MapEnv::new()
314 .with(DRIVE_CLIENT_ID, "id")
315 .with(DRIVE_CLIENT_SECRET, "secret");
316 let settings = settings_with(None, &[("work", account(None, None))]);
317 assert_eq!(
320 resolve_account(&raw, &settings, None).unwrap(),
321 ResolvedAccount::Named("work".to_string())
322 );
323 }
324
325 #[test]
326 fn resolve_explicit_account_selects_named() {
327 let settings = settings_with(None, &[("work", account(None, None))]);
328 assert_eq!(
329 resolve_account(&MapEnv::new(), &settings, Some("work")).unwrap(),
330 ResolvedAccount::Named("work".to_string())
331 );
332 }
333
334 #[test]
335 fn resolve_explicit_unknown_account_errors_with_sorted_list() {
336 let settings = settings_with(
337 None,
338 &[
339 ("work", account(None, None)),
340 ("personal", account(None, None)),
341 ],
342 );
343 let err = resolve_account(&MapEnv::new(), &settings, Some("bogus")).unwrap_err();
344 assert_eq!(
345 err.to_string(),
346 "unknown Drive account 'bogus'; known accounts: personal, work"
347 );
348 }
349
350 #[test]
351 fn resolve_explicit_unknown_account_reports_none_when_empty() {
352 let settings = settings_with(None, &[]);
353 let err = resolve_account(&MapEnv::new(), &settings, Some("bogus")).unwrap_err();
354 assert_eq!(
355 err.to_string(),
356 "unknown Drive account 'bogus'; known accounts: (none)"
357 );
358 }
359
360 #[test]
363 fn resolve_for_write_explicit_new_name_is_accepted_without_validation() {
364 let settings = settings_with(None, &[]);
367 assert_eq!(
368 resolve_account_for_write(&MapEnv::new(), &settings, Some("brand-new")).unwrap(),
369 ResolvedAccount::Named("brand-new".to_string())
370 );
371 }
372
373 #[test]
374 fn resolve_for_write_explicit_existing_name_still_selects_it() {
375 let settings = settings_with(None, &[("work", account(None, None))]);
376 assert_eq!(
377 resolve_account_for_write(&MapEnv::new(), &settings, Some("work")).unwrap(),
378 ResolvedAccount::Named("work".to_string())
379 );
380 }
381
382 #[test]
383 fn resolve_for_write_literal_env_still_bypasses_account_resolution() {
384 let raw = MapEnv::new()
385 .with(DRIVE_CLIENT_ID, "id")
386 .with(DRIVE_CLIENT_SECRET, "secret")
387 .with(DRIVE_REFRESH_TOKEN, "token");
388 let settings = settings_with(None, &[]);
389 assert_eq!(
390 resolve_account_for_write(&raw, &settings, Some("brand-new")).unwrap(),
391 ResolvedAccount::Unconfigured
392 );
393 }
394
395 #[test]
396 fn resolve_for_write_no_explicit_matches_read_side_fallback_rules() {
397 let sole = settings_with(None, &[("work", account(None, None))]);
400 assert_eq!(
401 resolve_account_for_write(&MapEnv::new(), &sole, None).unwrap(),
402 ResolvedAccount::Named("work".to_string())
403 );
404
405 let empty = settings_with(None, &[]);
406 assert_eq!(
407 resolve_account_for_write(&MapEnv::new(), &empty, None).unwrap(),
408 ResolvedAccount::Unconfigured
409 );
410
411 let ambiguous = settings_with(
412 None,
413 &[
414 ("work", account(None, None)),
415 ("personal", account(None, None)),
416 ],
417 );
418 assert!(resolve_account_for_write(&MapEnv::new(), &ambiguous, None).is_err());
419 }
420
421 #[test]
422 fn resolve_empty_accounts_is_unconfigured() {
423 let settings = settings_with(None, &[]);
424 assert_eq!(
425 resolve_account(&MapEnv::new(), &settings, None).unwrap(),
426 ResolvedAccount::Unconfigured
427 );
428 }
429
430 #[test]
431 fn resolve_no_explicit_uses_valid_default() {
432 let settings = settings_with(
433 Some("work"),
434 &[
435 ("work", account(None, None)),
436 ("personal", account(None, None)),
437 ],
438 );
439 assert_eq!(
440 resolve_account(&MapEnv::new(), &settings, None).unwrap(),
441 ResolvedAccount::Named("work".to_string())
442 );
443 }
444
445 #[test]
446 fn resolve_no_explicit_stale_default_errors() {
447 let settings = settings_with(Some("gone"), &[("work", account(None, None))]);
448 let err = resolve_account(&MapEnv::new(), &settings, None).unwrap_err();
449 assert_eq!(
450 err.to_string(),
451 "configured default Drive account 'gone' no longer exists; known accounts: work"
452 );
453 }
454
455 #[test]
456 fn resolve_no_explicit_no_default_falls_back_to_sole_account() {
457 let settings = settings_with(None, &[("work", account(None, None))]);
458 assert_eq!(
459 resolve_account(&MapEnv::new(), &settings, None).unwrap(),
460 ResolvedAccount::Named("work".to_string())
461 );
462 }
463
464 #[test]
465 fn resolve_no_explicit_no_default_multiple_accounts_errors() {
466 let settings = settings_with(
467 None,
468 &[
469 ("work", account(None, None)),
470 ("personal", account(None, None)),
471 ],
472 );
473 let err = resolve_account(&MapEnv::new(), &settings, None).unwrap_err();
474 assert_eq!(
475 err.to_string(),
476 "multiple Drive accounts configured and no default set; pass --account or run \
477 `drive account set-default <name>`"
478 );
479 }
480
481 #[test]
482 fn list_accounts_sorted_and_marks_default() {
483 let settings = settings_with(
484 Some("work"),
485 &[
486 (
487 "personal",
488 account(Some("me@gmail.com"), Some("drive.readonly")),
489 ),
490 ("work", account(Some("me@work.com"), Some("drive.readonly"))),
491 ],
492 );
493 let rows = list_accounts(&settings);
494 assert_eq!(rows.len(), 2);
495 assert_eq!(rows[0].name, "personal");
496 assert_eq!(rows[0].email_address.as_deref(), Some("me@gmail.com"));
497 assert_eq!(rows[0].scope.as_deref(), Some("drive.readonly"));
498 assert!(!rows[0].is_default);
499 assert_eq!(rows[1].name, "work");
500 assert!(rows[1].is_default);
501 }
502
503 #[test]
504 fn list_accounts_empty_when_no_accounts() {
505 assert!(list_accounts(&settings_with(None, &[])).is_empty());
506 }
507}