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