use anyhow::Result;
use serde::Serialize;
use crate::drive::auth::{DRIVE_CLIENT_ID, DRIVE_CLIENT_SECRET, DRIVE_REFRESH_TOKEN};
use crate::utils::env::EnvSource;
use crate::utils::settings::DriveSettings;
pub const DRIVE_ACCOUNT_ENV: &str = "OMNI_DEV_DRIVE_ACCOUNT";
pub fn active_drive_account_from<E: EnvSource>(raw: &E) -> Option<String> {
raw.var(DRIVE_ACCOUNT_ENV).filter(|s| !s.is_empty())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedAccount {
Named(String),
Unconfigured,
}
pub fn resolve_account<E: EnvSource>(
raw: &E,
settings: &DriveSettings,
explicit: Option<&str>,
) -> Result<ResolvedAccount> {
if has_literal_credentials(raw) {
return Ok(ResolvedAccount::Unconfigured);
}
if let Some(name) = explicit {
validate_account(settings, name)?;
return Ok(ResolvedAccount::Named(name.to_string()));
}
if settings.accounts.is_empty() {
return Ok(ResolvedAccount::Unconfigured);
}
resolve_default_account(settings).map(ResolvedAccount::Named)
}
pub fn resolve_account_for_write<E: EnvSource>(
raw: &E,
settings: &DriveSettings,
explicit: Option<&str>,
) -> Result<ResolvedAccount> {
if has_literal_credentials(raw) {
return Ok(ResolvedAccount::Unconfigured);
}
if let Some(name) = explicit {
return Ok(ResolvedAccount::Named(name.to_string()));
}
if settings.accounts.is_empty() {
return Ok(ResolvedAccount::Unconfigured);
}
resolve_default_account(settings).map(ResolvedAccount::Named)
}
fn has_literal_credentials<E: EnvSource>(raw: &E) -> bool {
raw.var(DRIVE_CLIENT_ID).is_some()
&& raw.var(DRIVE_CLIENT_SECRET).is_some()
&& raw.var(DRIVE_REFRESH_TOKEN).is_some()
}
pub fn validate_account(settings: &DriveSettings, name: &str) -> Result<()> {
if settings.accounts.contains_key(name) {
return Ok(());
}
Err(anyhow::anyhow!(
"unknown Drive account '{name}'; known accounts: {}",
known_accounts(settings)
))
}
fn known_accounts(settings: &DriveSettings) -> String {
if settings.accounts.is_empty() {
"(none)".to_string()
} else {
let mut names: Vec<&str> = settings.accounts.keys().map(String::as_str).collect();
names.sort_unstable();
names.join(", ")
}
}
fn resolve_default_account(settings: &DriveSettings) -> Result<String> {
if let Some(default) = &settings.default_account {
return if settings.accounts.contains_key(default) {
Ok(default.clone())
} else {
Err(anyhow::anyhow!(
"configured default Drive account '{default}' no longer exists; known accounts: {}",
known_accounts(settings)
))
};
}
let mut names = settings.accounts.keys();
if let (Some(sole), None) = (names.next(), names.next()) {
return Ok(sole.clone());
}
Err(anyhow::anyhow!(
"multiple Drive accounts configured and no default set; pass --account or run \
`drive account set-default <name>`"
))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AccountSummary {
pub name: String,
pub email_address: Option<String>,
pub scope: Option<String>,
pub is_default: bool,
}
pub fn list_accounts(settings: &DriveSettings) -> Vec<AccountSummary> {
let mut names: Vec<&String> = settings.accounts.keys().collect();
names.sort();
names
.into_iter()
.map(|name| {
let account = &settings.accounts[name];
AccountSummary {
name: name.clone(),
email_address: account.email_address.clone(),
scope: account.scope.clone(),
is_default: settings.default_account.as_deref() == Some(name.as_str()),
}
})
.collect()
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::test_support::env::MapEnv;
use crate::utils::settings::DriveAccountSettings;
use std::collections::HashMap;
fn account(email: Option<&str>, scope: Option<&str>) -> DriveAccountSettings {
DriveAccountSettings {
client_id: Some("id".to_string()),
client_secret: Some("secret".to_string()),
refresh_token: Some("token".to_string()),
scope: scope.map(str::to_string),
email_address: email.map(str::to_string),
chrome_profile_from_email: false,
browser_command: None,
}
}
fn settings_with(
default_account: Option<&str>,
accounts: &[(&str, DriveAccountSettings)],
) -> DriveSettings {
DriveSettings {
default_account: default_account.map(str::to_string),
accounts: accounts
.iter()
.map(|(name, acc)| {
(
(*name).to_string(),
DriveAccountSettings {
client_id: acc.client_id.clone(),
client_secret: acc.client_secret.clone(),
refresh_token: acc.refresh_token.clone(),
scope: acc.scope.clone(),
email_address: acc.email_address.clone(),
chrome_profile_from_email: acc.chrome_profile_from_email,
browser_command: acc.browser_command.clone(),
},
)
})
.collect::<HashMap<_, _>>(),
}
}
#[test]
fn active_drive_account_from_reads_and_trims_empty() {
assert_eq!(
active_drive_account_from(&MapEnv::new().with(DRIVE_ACCOUNT_ENV, "work")),
Some("work".to_string())
);
assert_eq!(
active_drive_account_from(&MapEnv::new().with(DRIVE_ACCOUNT_ENV, "")),
None
);
assert_eq!(active_drive_account_from(&MapEnv::new()), None);
}
#[test]
fn resolve_literal_env_bypasses_account_resolution() {
let raw = MapEnv::new()
.with(DRIVE_CLIENT_ID, "id")
.with(DRIVE_CLIENT_SECRET, "secret")
.with(DRIVE_REFRESH_TOKEN, "token");
let settings = settings_with(
None,
&[("a", account(None, None)), ("b", account(None, None))],
);
assert_eq!(
resolve_account(&raw, &settings, Some("a")).unwrap(),
ResolvedAccount::Unconfigured
);
assert_eq!(
resolve_account(&raw, &settings, None).unwrap(),
ResolvedAccount::Unconfigured
);
}
#[test]
fn resolve_literal_env_requires_all_three_keys() {
let raw = MapEnv::new()
.with(DRIVE_CLIENT_ID, "id")
.with(DRIVE_CLIENT_SECRET, "secret");
let settings = settings_with(None, &[("work", account(None, None))]);
assert_eq!(
resolve_account(&raw, &settings, None).unwrap(),
ResolvedAccount::Named("work".to_string())
);
}
#[test]
fn resolve_explicit_account_selects_named() {
let settings = settings_with(None, &[("work", account(None, None))]);
assert_eq!(
resolve_account(&MapEnv::new(), &settings, Some("work")).unwrap(),
ResolvedAccount::Named("work".to_string())
);
}
#[test]
fn resolve_explicit_unknown_account_errors_with_sorted_list() {
let settings = settings_with(
None,
&[
("work", account(None, None)),
("personal", account(None, None)),
],
);
let err = resolve_account(&MapEnv::new(), &settings, Some("bogus")).unwrap_err();
assert_eq!(
err.to_string(),
"unknown Drive account 'bogus'; known accounts: personal, work"
);
}
#[test]
fn resolve_explicit_unknown_account_reports_none_when_empty() {
let settings = settings_with(None, &[]);
let err = resolve_account(&MapEnv::new(), &settings, Some("bogus")).unwrap_err();
assert_eq!(
err.to_string(),
"unknown Drive account 'bogus'; known accounts: (none)"
);
}
#[test]
fn resolve_for_write_explicit_new_name_is_accepted_without_validation() {
let settings = settings_with(None, &[]);
assert_eq!(
resolve_account_for_write(&MapEnv::new(), &settings, Some("brand-new")).unwrap(),
ResolvedAccount::Named("brand-new".to_string())
);
}
#[test]
fn resolve_for_write_explicit_existing_name_still_selects_it() {
let settings = settings_with(None, &[("work", account(None, None))]);
assert_eq!(
resolve_account_for_write(&MapEnv::new(), &settings, Some("work")).unwrap(),
ResolvedAccount::Named("work".to_string())
);
}
#[test]
fn resolve_for_write_literal_env_still_bypasses_account_resolution() {
let raw = MapEnv::new()
.with(DRIVE_CLIENT_ID, "id")
.with(DRIVE_CLIENT_SECRET, "secret")
.with(DRIVE_REFRESH_TOKEN, "token");
let settings = settings_with(None, &[]);
assert_eq!(
resolve_account_for_write(&raw, &settings, Some("brand-new")).unwrap(),
ResolvedAccount::Unconfigured
);
}
#[test]
fn resolve_for_write_no_explicit_matches_read_side_fallback_rules() {
let sole = settings_with(None, &[("work", account(None, None))]);
assert_eq!(
resolve_account_for_write(&MapEnv::new(), &sole, None).unwrap(),
ResolvedAccount::Named("work".to_string())
);
let empty = settings_with(None, &[]);
assert_eq!(
resolve_account_for_write(&MapEnv::new(), &empty, None).unwrap(),
ResolvedAccount::Unconfigured
);
let ambiguous = settings_with(
None,
&[
("work", account(None, None)),
("personal", account(None, None)),
],
);
assert!(resolve_account_for_write(&MapEnv::new(), &ambiguous, None).is_err());
}
#[test]
fn resolve_empty_accounts_is_unconfigured() {
let settings = settings_with(None, &[]);
assert_eq!(
resolve_account(&MapEnv::new(), &settings, None).unwrap(),
ResolvedAccount::Unconfigured
);
}
#[test]
fn resolve_no_explicit_uses_valid_default() {
let settings = settings_with(
Some("work"),
&[
("work", account(None, None)),
("personal", account(None, None)),
],
);
assert_eq!(
resolve_account(&MapEnv::new(), &settings, None).unwrap(),
ResolvedAccount::Named("work".to_string())
);
}
#[test]
fn resolve_no_explicit_stale_default_errors() {
let settings = settings_with(Some("gone"), &[("work", account(None, None))]);
let err = resolve_account(&MapEnv::new(), &settings, None).unwrap_err();
assert_eq!(
err.to_string(),
"configured default Drive account 'gone' no longer exists; known accounts: work"
);
}
#[test]
fn resolve_no_explicit_no_default_falls_back_to_sole_account() {
let settings = settings_with(None, &[("work", account(None, None))]);
assert_eq!(
resolve_account(&MapEnv::new(), &settings, None).unwrap(),
ResolvedAccount::Named("work".to_string())
);
}
#[test]
fn resolve_no_explicit_no_default_multiple_accounts_errors() {
let settings = settings_with(
None,
&[
("work", account(None, None)),
("personal", account(None, None)),
],
);
let err = resolve_account(&MapEnv::new(), &settings, None).unwrap_err();
assert_eq!(
err.to_string(),
"multiple Drive accounts configured and no default set; pass --account or run \
`drive account set-default <name>`"
);
}
#[test]
fn list_accounts_sorted_and_marks_default() {
let settings = settings_with(
Some("work"),
&[
(
"personal",
account(Some("me@gmail.com"), Some("drive.readonly")),
),
("work", account(Some("me@work.com"), Some("drive.readonly"))),
],
);
let rows = list_accounts(&settings);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].name, "personal");
assert_eq!(rows[0].email_address.as_deref(), Some("me@gmail.com"));
assert_eq!(rows[0].scope.as_deref(), Some("drive.readonly"));
assert!(!rows[0].is_default);
assert_eq!(rows[1].name, "work");
assert!(rows[1].is_default);
}
#[test]
fn list_accounts_empty_when_no_accounts() {
assert!(list_accounts(&settings_with(None, &[])).is_empty());
}
}