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 }
238 }
239
240 fn settings_with(
241 default_account: Option<&str>,
242 accounts: &[(&str, DriveAccountSettings)],
243 ) -> DriveSettings {
244 DriveSettings {
245 default_account: default_account.map(str::to_string),
246 accounts: accounts
247 .iter()
248 .map(|(name, acc)| {
249 (
250 (*name).to_string(),
251 DriveAccountSettings {
252 client_id: acc.client_id.clone(),
253 client_secret: acc.client_secret.clone(),
254 refresh_token: acc.refresh_token.clone(),
255 scope: acc.scope.clone(),
256 email_address: acc.email_address.clone(),
257 chrome_profile_from_email: acc.chrome_profile_from_email,
258 browser_command: acc.browser_command.clone(),
259 },
260 )
261 })
262 .collect::<HashMap<_, _>>(),
263 }
264 }
265
266 #[test]
267 fn active_drive_account_from_reads_and_trims_empty() {
268 assert_eq!(
269 active_drive_account_from(&MapEnv::new().with(DRIVE_ACCOUNT_ENV, "work")),
270 Some("work".to_string())
271 );
272 assert_eq!(
273 active_drive_account_from(&MapEnv::new().with(DRIVE_ACCOUNT_ENV, "")),
274 None
275 );
276 assert_eq!(active_drive_account_from(&MapEnv::new()), None);
277 }
278
279 #[test]
280 fn resolve_literal_env_bypasses_account_resolution() {
281 let raw = MapEnv::new()
282 .with(DRIVE_CLIENT_ID, "id")
283 .with(DRIVE_CLIENT_SECRET, "secret")
284 .with(DRIVE_REFRESH_TOKEN, "token");
285 let settings = settings_with(
288 None,
289 &[("a", account(None, None)), ("b", account(None, None))],
290 );
291 assert_eq!(
292 resolve_account(&raw, &settings, Some("a")).unwrap(),
293 ResolvedAccount::Unconfigured
294 );
295 assert_eq!(
296 resolve_account(&raw, &settings, None).unwrap(),
297 ResolvedAccount::Unconfigured
298 );
299 }
300
301 #[test]
302 fn resolve_literal_env_requires_all_three_keys() {
303 let raw = MapEnv::new()
304 .with(DRIVE_CLIENT_ID, "id")
305 .with(DRIVE_CLIENT_SECRET, "secret");
306 let settings = settings_with(None, &[("work", account(None, None))]);
307 assert_eq!(
310 resolve_account(&raw, &settings, None).unwrap(),
311 ResolvedAccount::Named("work".to_string())
312 );
313 }
314
315 #[test]
316 fn resolve_explicit_account_selects_named() {
317 let settings = settings_with(None, &[("work", account(None, None))]);
318 assert_eq!(
319 resolve_account(&MapEnv::new(), &settings, Some("work")).unwrap(),
320 ResolvedAccount::Named("work".to_string())
321 );
322 }
323
324 #[test]
325 fn resolve_explicit_unknown_account_errors_with_sorted_list() {
326 let settings = settings_with(
327 None,
328 &[
329 ("work", account(None, None)),
330 ("personal", account(None, None)),
331 ],
332 );
333 let err = resolve_account(&MapEnv::new(), &settings, Some("bogus")).unwrap_err();
334 assert_eq!(
335 err.to_string(),
336 "unknown Drive account 'bogus'; known accounts: personal, work"
337 );
338 }
339
340 #[test]
341 fn resolve_explicit_unknown_account_reports_none_when_empty() {
342 let settings = settings_with(None, &[]);
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: (none)"
347 );
348 }
349
350 #[test]
353 fn resolve_for_write_explicit_new_name_is_accepted_without_validation() {
354 let settings = settings_with(None, &[]);
357 assert_eq!(
358 resolve_account_for_write(&MapEnv::new(), &settings, Some("brand-new")).unwrap(),
359 ResolvedAccount::Named("brand-new".to_string())
360 );
361 }
362
363 #[test]
364 fn resolve_for_write_explicit_existing_name_still_selects_it() {
365 let settings = settings_with(None, &[("work", account(None, None))]);
366 assert_eq!(
367 resolve_account_for_write(&MapEnv::new(), &settings, Some("work")).unwrap(),
368 ResolvedAccount::Named("work".to_string())
369 );
370 }
371
372 #[test]
373 fn resolve_for_write_literal_env_still_bypasses_account_resolution() {
374 let raw = MapEnv::new()
375 .with(DRIVE_CLIENT_ID, "id")
376 .with(DRIVE_CLIENT_SECRET, "secret")
377 .with(DRIVE_REFRESH_TOKEN, "token");
378 let settings = settings_with(None, &[]);
379 assert_eq!(
380 resolve_account_for_write(&raw, &settings, Some("brand-new")).unwrap(),
381 ResolvedAccount::Unconfigured
382 );
383 }
384
385 #[test]
386 fn resolve_for_write_no_explicit_matches_read_side_fallback_rules() {
387 let sole = settings_with(None, &[("work", account(None, None))]);
390 assert_eq!(
391 resolve_account_for_write(&MapEnv::new(), &sole, None).unwrap(),
392 ResolvedAccount::Named("work".to_string())
393 );
394
395 let empty = settings_with(None, &[]);
396 assert_eq!(
397 resolve_account_for_write(&MapEnv::new(), &empty, None).unwrap(),
398 ResolvedAccount::Unconfigured
399 );
400
401 let ambiguous = settings_with(
402 None,
403 &[
404 ("work", account(None, None)),
405 ("personal", account(None, None)),
406 ],
407 );
408 assert!(resolve_account_for_write(&MapEnv::new(), &ambiguous, None).is_err());
409 }
410
411 #[test]
412 fn resolve_empty_accounts_is_unconfigured() {
413 let settings = settings_with(None, &[]);
414 assert_eq!(
415 resolve_account(&MapEnv::new(), &settings, None).unwrap(),
416 ResolvedAccount::Unconfigured
417 );
418 }
419
420 #[test]
421 fn resolve_no_explicit_uses_valid_default() {
422 let settings = settings_with(
423 Some("work"),
424 &[
425 ("work", account(None, None)),
426 ("personal", account(None, None)),
427 ],
428 );
429 assert_eq!(
430 resolve_account(&MapEnv::new(), &settings, None).unwrap(),
431 ResolvedAccount::Named("work".to_string())
432 );
433 }
434
435 #[test]
436 fn resolve_no_explicit_stale_default_errors() {
437 let settings = settings_with(Some("gone"), &[("work", account(None, None))]);
438 let err = resolve_account(&MapEnv::new(), &settings, None).unwrap_err();
439 assert_eq!(
440 err.to_string(),
441 "configured default Drive account 'gone' no longer exists; known accounts: work"
442 );
443 }
444
445 #[test]
446 fn resolve_no_explicit_no_default_falls_back_to_sole_account() {
447 let settings = settings_with(None, &[("work", account(None, None))]);
448 assert_eq!(
449 resolve_account(&MapEnv::new(), &settings, None).unwrap(),
450 ResolvedAccount::Named("work".to_string())
451 );
452 }
453
454 #[test]
455 fn resolve_no_explicit_no_default_multiple_accounts_errors() {
456 let settings = settings_with(
457 None,
458 &[
459 ("work", account(None, None)),
460 ("personal", account(None, None)),
461 ],
462 );
463 let err = resolve_account(&MapEnv::new(), &settings, None).unwrap_err();
464 assert_eq!(
465 err.to_string(),
466 "multiple Drive accounts configured and no default set; pass --account or run \
467 `drive account set-default <name>`"
468 );
469 }
470
471 #[test]
472 fn list_accounts_sorted_and_marks_default() {
473 let settings = settings_with(
474 Some("work"),
475 &[
476 (
477 "personal",
478 account(Some("me@gmail.com"), Some("drive.readonly")),
479 ),
480 ("work", account(Some("me@work.com"), Some("drive.readonly"))),
481 ],
482 );
483 let rows = list_accounts(&settings);
484 assert_eq!(rows.len(), 2);
485 assert_eq!(rows[0].name, "personal");
486 assert_eq!(rows[0].email_address.as_deref(), Some("me@gmail.com"));
487 assert_eq!(rows[0].scope.as_deref(), Some("drive.readonly"));
488 assert!(!rows[0].is_default);
489 assert_eq!(rows[1].name, "work");
490 assert!(rows[1].is_default);
491 }
492
493 #[test]
494 fn list_accounts_empty_when_no_accounts() {
495 assert!(list_accounts(&settings_with(None, &[])).is_empty());
496 }
497}