pub(crate) mod account;
pub(crate) mod auth;
pub(crate) mod create;
pub(crate) mod dedupe;
pub(crate) mod docs;
pub(crate) mod edit;
pub(crate) mod format;
pub(crate) mod helpers;
pub(crate) mod lease;
pub(crate) mod move_file;
pub(crate) mod permissions;
pub(crate) mod read;
pub(crate) mod rename;
pub(crate) mod search;
pub(crate) mod sheets;
pub(crate) mod slides;
pub(crate) mod sync;
pub(crate) mod trash;
pub(crate) mod upload;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::drive::account::DRIVE_ACCOUNT_ENV;
use crate::drive::client::DriveClient;
#[derive(Parser)]
pub struct DriveCommand {
#[arg(long, global = true, value_name = "NAME")]
pub account: Option<String>,
#[command(subcommand)]
pub command: DriveSubcommands,
}
#[derive(Subcommand)]
pub enum DriveSubcommands {
Auth(auth::AuthCommand),
Account(account::AccountCommand),
Search(search::SearchCommand),
Read(read::ReadCommand),
Dedupe(dedupe::DedupeCommand),
Sync(sync::SyncCommand),
Create(create::CreateCommand),
Upload(upload::UploadCommand),
Edit(edit::EditCommand),
Trash(trash::TrashCommand),
Untrash(trash::TrashCommand),
Lease(lease::LeaseCommand),
Rename(rename::RenameCommand),
Move(move_file::MoveCommand),
Permissions(permissions::PermissionsCommand),
Docs(docs::DocsCommand),
Slides(slides::SlidesCommand),
Sheets(sheets::SheetsCommand),
}
impl DriveCommand {
pub async fn execute(self) -> Result<()> {
let _account_guard = self
.account
.as_ref()
.map(|account| crate::utils::env::ScopedEnvVar::set(DRIVE_ACCOUNT_ENV, account));
match self.command {
DriveSubcommands::Auth(cmd) => cmd.execute().await,
DriveSubcommands::Account(cmd) => cmd.execute(),
DriveSubcommands::Permissions(cmd) => cmd.execute().await,
DriveSubcommands::Lease(cmd) => cmd.execute(helpers::create_client).await,
command => {
let client = helpers::create_client()?;
command.dispatch(&client).await
}
}
}
}
impl DriveSubcommands {
async fn dispatch(self, client: &DriveClient) -> Result<()> {
match self {
Self::Auth(_) => unreachable!("Auth is dispatched before client resolution"),
Self::Account(_) => unreachable!("Account is dispatched before client resolution"),
Self::Permissions(_) => {
unreachable!("Permissions is dispatched before client resolution")
}
Self::Lease(_) => unreachable!("Lease is dispatched before client resolution"),
Self::Search(cmd) => cmd.execute(client).await,
Self::Read(cmd) => cmd.execute(client).await,
Self::Dedupe(cmd) => cmd.execute(client).await,
Self::Sync(cmd) => cmd.execute(client).await,
Self::Create(cmd) => cmd.execute(client).await,
Self::Upload(cmd) => cmd.execute(client).await,
Self::Edit(cmd) => cmd.execute(client).await,
Self::Trash(cmd) => cmd.execute(client, false).await,
Self::Untrash(cmd) => cmd.execute(client, true).await,
Self::Rename(cmd) => cmd.execute(client).await,
Self::Move(cmd) => cmd.execute(client).await,
Self::Docs(cmd) => cmd.execute(client).await,
Self::Slides(cmd) => cmd.execute(client).await,
Self::Sheets(cmd) => cmd.execute(client).await,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::cli::drive::format::OutputFormat;
use crate::drive::auth::{DriveCredentials, DriveGrantedScopes};
use crate::utils::secret::Secret;
fn dead_credentials() -> DriveCredentials {
DriveCredentials {
client_id: "client".to_string(),
client_secret: Secret::new("secret"),
refresh_token: Secret::new("refresh"),
scope: DriveGrantedScopes::READONLY,
}
}
fn dead_client() -> DriveClient {
DriveClient::new("http://127.0.0.1:1", &dead_credentials()).unwrap()
}
fn no_write_args() -> helpers::StructureWriteArgs {
helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}
}
#[tokio::test]
async fn execute_routes_auth_subcommand_and_surfaces_missing_credentials() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Auth(auth::AuthCommand {
command: auth::AuthSubcommands::Status(auth::StatusCommand { all: false }),
}),
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn execute_non_auth_subcommand_errors_when_credentials_missing() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Search(search::SearchCommand {
query: "name contains 'report'".to_string(),
limit: 10,
output: OutputFormat::Table,
}),
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn execute_lease_release_needs_no_credentials() {
let guard = crate::drive::test_support::EnvGuard::take();
let dir = guard.clear_credentials();
let audit = crate::test_support::AuditLogGuard::redirect(dir.path());
let cmd = lease::LeaseCommand::release_for_test("no-such-token".to_string());
let code = cmd
.run(|| panic!("release must never resolve a client"))
.await
.unwrap();
assert_eq!(code, 1, "NoSuchToken must be a non-zero exit");
assert_eq!(audit.verdicts(), vec!["release-no-such-token".to_string()]);
}
#[tokio::test]
async fn execute_lease_prune_dry_run_needs_no_credentials() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Lease(lease::LeaseCommand::prune_for_test("1h", true)),
};
cmd.execute().await.unwrap();
}
#[tokio::test]
async fn execute_lease_acquire_still_errors_when_credentials_missing() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Lease(lease::LeaseCommand::acquire_for_test(
"file-1".to_string(),
)),
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"), "{err}");
}
#[tokio::test]
async fn execute_restores_account_env_var_after_return() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: Some("work".to_string()),
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
}
#[tokio::test]
async fn execute_restores_previous_account_env_var_after_return() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
let cmd = DriveCommand {
account: Some("work".to_string()),
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
assert_eq!(
std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
Some("personal")
);
}
#[tokio::test]
async fn execute_does_not_leak_account_across_sequential_calls() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let account_list_cmd = || DriveCommand {
account: None,
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
let first = DriveCommand {
account: Some("alpha".to_string()),
..account_list_cmd()
};
first.execute().await.unwrap();
assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
account_list_cmd().execute().await.unwrap();
assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
}
#[tokio::test]
async fn execute_absent_account_leaves_ambient_env_var_untouched() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
assert_eq!(
std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
Some("personal")
);
}
#[tokio::test]
async fn execute_routes_account_list_without_client_resolution() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
}
#[tokio::test]
async fn execute_routes_permissions_show_without_client_resolution() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Permissions(permissions::PermissionsCommand {
command: permissions::PermissionsSubcommands::Show(
permissions::show::ShowCommand {
output: OutputFormat::Table,
},
),
}),
};
cmd.execute().await.unwrap();
}
#[tokio::test]
async fn execute_routes_permissions_check_and_surfaces_missing_credentials() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Permissions(permissions::PermissionsCommand {
command: permissions::PermissionsSubcommands::Check(
permissions::check::CheckCommand {
id: "f1".to_string(),
operation: permissions::check::OperationArg::Read,
output: OutputFormat::Table,
},
),
}),
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn dispatch_routes_slides_read_verbs_to_the_slides_host() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
for command in [
slides::SlidesSubcommands::Info(slides::info::InfoCommand {
presentation_id: "p".into(),
output: OutputFormat::Table,
}),
slides::SlidesSubcommands::Read(slides::read::ReadCommand {
presentation_id: "p".into(),
slides: vec![],
output: OutputFormat::Table,
}),
] {
let cmd = DriveSubcommands::Slides(slides::SlidesCommand { command });
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
}
#[tokio::test]
async fn dispatch_routes_docs_info() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::Info(docs::info::InfoCommand {
document_id: "d1".to_string(),
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_docs_read() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::Read(docs::read::ReadCommand {
document_id: "d1".to_string(),
tab: None,
suggestions_view: docs::read::SuggestionsViewArg::Default,
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_docs_replace() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::Replace(docs::write::ReplaceCommand {
document_id: "d1".to_string(),
search: "a".to_string(),
replace: "b".to_string(),
ignore_case: false,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_docs_append() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::Append(docs::write::AppendCommand {
document_id: "d1".to_string(),
text: Some("x".to_string()),
text_file: None,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
fn insert_command(
before: Option<&str>,
after: Option<&str>,
text: Option<&str>,
text_file: Option<&str>,
) -> DriveSubcommands {
DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::Insert(docs::write::InsertCommand {
segment_id: None,
tab_id: None,
document_id: "d1".to_string(),
before: before.map(str::to_string),
after: after.map(str::to_string),
text: text.map(str::to_string),
text_file: text_file.map(str::to_string),
ignore_case: true,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
})
}
fn delete_command(
match_text: Option<&str>,
from: Option<&str>,
to: Option<&str>,
) -> DriveSubcommands {
DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::Delete(docs::write::DeleteCommand {
segment_id: None,
tab_id: None,
document_id: "d1".to_string(),
match_text: match_text.map(str::to_string),
from: from.map(str::to_string),
to: to.map(str::to_string),
ignore_case: true,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
})
}
#[tokio::test]
async fn dispatch_routes_docs_insert_for_every_anchor_side_and_text_source() {
let guard = crate::drive::test_support::EnvGuard::take();
let dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
let file = dir.path().join("insert.txt");
std::fs::write(&file, "from a file").unwrap();
let file = file.to_str().unwrap();
for cmd in [
insert_command(Some("a"), None, Some("x"), None),
insert_command(None, Some("a"), Some("x"), None),
insert_command(Some("a"), None, None, Some(file)),
insert_command(None, Some("a"), None, Some(file)),
] {
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
}
#[tokio::test]
async fn dispatch_refuses_an_insert_without_exactly_one_anchor_and_text_source() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for cmd in [
insert_command(None, None, Some("x"), None),
insert_command(Some("a"), Some("b"), Some("x"), None),
] {
let err = cmd.dispatch(&dead_client()).await.unwrap_err();
assert!(err.to_string().contains("--before or --after"), "{err}");
}
for cmd in [
insert_command(Some("a"), None, None, None),
insert_command(Some("a"), None, Some("x"), Some("f")),
] {
let err = cmd.dispatch(&dead_client()).await.unwrap_err();
assert!(err.to_string().contains("--text or --text-file"), "{err}");
}
}
#[tokio::test]
async fn dispatch_routes_docs_delete_for_a_match_and_for_a_range() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for cmd in [
delete_command(Some("a"), None, None),
delete_command(None, Some("a"), Some("b")),
] {
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
}
#[tokio::test]
async fn dispatch_refuses_a_delete_that_is_not_a_match_or_a_full_range() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for cmd in [
delete_command(None, None, None),
delete_command(None, Some("a"), None),
delete_command(None, None, Some("b")),
delete_command(Some("a"), Some("a"), Some("b")),
delete_command(Some("a"), Some("a"), None),
] {
let err = cmd.dispatch(&dead_client()).await.unwrap_err();
assert!(
err.to_string().contains("--match or both --from and --to"),
"{err}"
);
}
}
fn list_selection(
match_text: Option<&str>,
from: Option<&str>,
to: Option<&str>,
) -> docs::write::ListSelection {
docs::write::ListSelection {
document_id: "d1".to_string(),
match_text: match_text.map(str::to_string),
from: from.map(str::to_string),
to: to.map(str::to_string),
ignore_case: true,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}
}
fn create_bullets_command(selection: docs::write::ListSelection) -> DriveSubcommands {
DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::CreateBullets(docs::write::CreateBulletsCommand {
selection,
preset: docs::write::BulletPresetArg::BulletCheckbox,
}),
})
}
fn delete_bullets_command(selection: docs::write::ListSelection) -> DriveSubcommands {
DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::DeleteBullets(docs::write::DeleteBulletsCommand {
selection,
}),
})
}
#[tokio::test]
async fn dispatch_routes_docs_create_and_delete_bullets() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for cmd in [
create_bullets_command(list_selection(Some("a"), None, None)),
create_bullets_command(list_selection(None, Some("a"), Some("b"))),
delete_bullets_command(list_selection(Some("a"), None, None)),
delete_bullets_command(list_selection(None, Some("a"), Some("b"))),
] {
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
}
#[tokio::test]
async fn dispatch_refuses_a_list_verb_that_is_not_a_match_or_a_full_range() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for (match_text, from, to) in [
(None, None, None),
(None, Some("a"), None),
(None, None, Some("b")),
(Some("a"), Some("a"), Some("b")),
(Some("a"), Some("a"), None),
] {
for cmd in [
create_bullets_command(list_selection(match_text, from, to)),
delete_bullets_command(list_selection(match_text, from, to)),
] {
let err = cmd.dispatch(&dead_client()).await.unwrap_err();
assert!(
err.to_string().contains("--match or both --from and --to"),
"{err}"
);
}
}
}
fn style_selection(
match_text: Option<&str>,
from: Option<&str>,
to: Option<&str>,
) -> docs::style::Selection {
docs::style::Selection {
document_id: "d1".to_string(),
match_text: match_text.map(str::to_string),
from: from.map(str::to_string),
to: to.map(str::to_string),
ignore_case: true,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}
}
fn text_style_command(
match_text: Option<&str>,
from: Option<&str>,
to: Option<&str>,
) -> DriveSubcommands {
DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::TextStyle(docs::style::TextStyleCommand {
selection: style_selection(match_text, from, to),
bold: Some(true),
italic: None,
underline: None,
strikethrough: None,
}),
})
}
fn paragraph_style_command(
match_text: Option<&str>,
from: Option<&str>,
to: Option<&str>,
) -> DriveSubcommands {
use crate::drive::docs::style::{Alignment, NamedStyle};
DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::ParagraphStyle(docs::style::ParagraphStyleCommand {
selection: style_selection(match_text, from, to),
alignment: Some(Alignment::Center),
named_style: Some(NamedStyle::Title),
}),
})
}
#[tokio::test]
async fn dispatch_routes_docs_text_style_and_paragraph_style_for_a_match_and_for_a_range() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for cmd in [
text_style_command(Some("a"), None, None),
text_style_command(None, Some("a"), Some("b")),
paragraph_style_command(Some("a"), None, None),
paragraph_style_command(None, Some("a"), Some("b")),
] {
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
}
#[tokio::test]
async fn dispatch_refuses_a_format_that_is_not_a_match_or_a_full_range() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for (match_text, from, to) in [
(None, None, None),
(None, Some("a"), None),
(None, None, Some("b")),
(Some("a"), Some("a"), Some("b")),
(Some("a"), Some("a"), None),
] {
for cmd in [
text_style_command(match_text, from, to),
paragraph_style_command(match_text, from, to),
] {
let err = cmd.dispatch(&dead_client()).await.unwrap_err();
assert!(
err.to_string().contains("--match or both --from and --to"),
"{err}"
);
}
}
}
fn table_common() -> docs::table::Common {
docs::table::Common {
document_id: "d1".to_string(),
dry_run: false,
ignore_case: true,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}
}
fn insert_table_command(before: Option<&str>, after: Option<&str>) -> DriveSubcommands {
DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::InsertTable(docs::table::InsertTableCommand {
common: table_common(),
before: before.map(str::to_string),
after: after.map(str::to_string),
rows: 2,
columns: 3,
}),
})
}
fn insert_dimension(before: bool, after: bool) -> docs::table::InsertDimensionCommand {
docs::table::InsertDimensionCommand {
common: table_common(),
cell: "a".to_string(),
before,
after,
}
}
fn delete_dimension() -> docs::table::DeleteDimensionCommand {
docs::table::DeleteDimensionCommand {
common: table_common(),
cell: "a".to_string(),
}
}
#[tokio::test]
async fn dispatch_routes_every_docs_table_verb() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
let wrap = |command| DriveSubcommands::Docs(docs::DocsCommand { command });
for cmd in [
insert_table_command(Some("a"), None),
insert_table_command(None, Some("a")),
wrap(docs::DocsSubcommands::InsertTableRow(insert_dimension(
true, false,
))),
wrap(docs::DocsSubcommands::InsertTableRow(insert_dimension(
false, true,
))),
wrap(docs::DocsSubcommands::InsertTableColumn(insert_dimension(
true, false,
))),
wrap(docs::DocsSubcommands::InsertTableColumn(insert_dimension(
false, true,
))),
wrap(docs::DocsSubcommands::DeleteTableRow(delete_dimension())),
wrap(docs::DocsSubcommands::DeleteTableColumn(delete_dimension())),
] {
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
}
#[tokio::test]
async fn dispatch_refuses_an_insert_table_without_exactly_one_anchor_side() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for (before, after) in [(None, None), (Some("a"), Some("b"))] {
let err = insert_table_command(before, after)
.dispatch(&dead_client())
.await
.unwrap_err();
assert!(
err.to_string()
.contains("exactly one of --before or --after"),
"{err}"
);
}
}
fn scoped_args() -> docs::named_range::ScopedArgs {
docs::named_range::ScopedArgs {
document_id: "d1".to_string(),
tab: "legacy".to_string(),
segment: "body".to_string(),
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}
}
fn named_range_command(command: docs::DocsSubcommands) -> DriveSubcommands {
DriveSubcommands::Docs(docs::DocsCommand { command })
}
fn replace_named_range_content(
text: Option<&str>,
text_file: Option<&str>,
) -> DriveSubcommands {
named_range_command(docs::DocsSubcommands::ReplaceNamedRangeContent(
docs::named_range::ReplaceNamedRangeContentCommand {
scope: scoped_args(),
id: "nr".to_string(),
text: text.map(str::to_string),
text_file: text_file.map(str::to_string),
},
))
}
#[tokio::test]
async fn dispatch_routes_docs_named_range_verbs() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
let file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(file.path(), "from a file").unwrap();
for cmd in [
named_range_command(docs::DocsSubcommands::CreateNamedRange(
docs::named_range::CreateNamedRangeCommand {
scope: scoped_args(),
name: "label".to_string(),
start_index: 1,
end_index: 3,
},
)),
named_range_command(docs::DocsSubcommands::DeleteNamedRange(
docs::named_range::DeleteNamedRangeCommand {
scope: scoped_args(),
id: "nr".to_string(),
},
)),
replace_named_range_content(Some("inline"), None),
replace_named_range_content(None, Some(file.path().to_str().unwrap())),
] {
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
}
#[tokio::test]
async fn dispatch_refuses_named_range_replacement_without_exactly_one_text_source() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
guard.redirect_api_hosts_to_a_dead_port();
for (text, text_file) in [(None, None), (Some("x"), Some("f"))] {
let err = replace_named_range_content(text, text_file)
.dispatch(&dead_client())
.await
.unwrap_err();
assert!(
err.to_string()
.contains("exactly one of --text or --text-file"),
"{err}"
);
}
let err = replace_named_range_content(None, Some("/nonexistent/no-such-file.txt"))
.dispatch(&dead_client())
.await
.unwrap_err();
assert!(err.to_string().contains("Failed to stat"), "{err}");
}
#[tokio::test]
async fn dispatch_routes_docs_create() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Docs(docs::DocsCommand {
command: docs::DocsSubcommands::Create(docs::create::CreateCommand {
name: "n".to_string(),
parent: "folder-1".to_string(),
text: None,
text_file: None,
dry_run: false,
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_trash_and_untrash() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let leaf = || trash::TrashCommand {
file_id: "file-1".to_string(),
dry_run: true,
output: OutputFormat::Table,
};
assert!(DriveSubcommands::Trash(leaf())
.dispatch(&dead_client())
.await
.is_ok());
assert!(DriveSubcommands::Untrash(leaf())
.dispatch(&dead_client())
.await
.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_info() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::Info(sheets::info::InfoCommand {
spreadsheet_id: "s1".to_string(),
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_sheets_read() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::Read(sheets::read::ReadCommand {
spreadsheet_id: "s1".to_string(),
range: None,
sheet: None,
render: sheets::read::RenderArg::Formatted,
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_sheets_write() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::Write(sheets::write::WriteCommand {
spreadsheet_id: "s1".to_string(),
range: Some("A1".to_string()),
sheet: None,
values: "/definitely/not/here.csv".to_string(),
values_format: sheets::values::ValuesFormat::Auto,
input: sheets::write::InputArg::UserEntered,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_sheets_append() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::Append(sheets::write::AppendCommand {
spreadsheet_id: "s1".to_string(),
range: Some("A1".to_string()),
sheet: None,
values: "/definitely/not/here.csv".to_string(),
values_format: sheets::values::ValuesFormat::Auto,
input: sheets::write::InputArg::UserEntered,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_sheets_clear() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::Clear(sheets::write::ClearCommand {
spreadsheet_id: "s1".to_string(),
range: Some("A1".to_string()),
sheet: None,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_create() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::Create(sheets::create::CreateCommand {
name: "Budget".to_string(),
parent: "parent-1".to_string(),
values: None,
values_format: sheets::values::ValuesFormat::Auto,
input: sheets::write::InputArg::UserEntered,
dry_run: false,
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_add_sheet() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::AddSheet(sheets::structure::AddSheetCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
title: "Q3".to_string(),
index: None,
rows: None,
columns: None,
write: no_write_args(),
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_rename_sheet() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::RenameSheet(
sheets::structure::RenameSheetCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
title: "Q3".to_string(),
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_insert_rows() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::InsertRows(sheets::structure::InsertRowsCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
at: 5,
count: 3,
write: no_write_args(),
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_insert_columns() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::InsertColumns(
sheets::structure::InsertColumnsCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
at: 2,
count: 1,
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_move_rows() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::MoveRows(sheets::structure::MoveRowsCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
at: 5,
count: 3,
before: 10,
write: no_write_args(),
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_move_columns() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::MoveColumns(
sheets::structure::MoveColumnsCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
at: 5,
count: 3,
before: 10,
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_move_chart() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::MoveChart(
sheets::embedded_object::MoveChartCommand {
spreadsheet_id: "sheet-1".to_string(),
chart_id: 1,
sheet: None,
anchor: Some("F2".to_string()),
offset_x: None,
offset_y: None,
width: None,
height: None,
new_sheet: false,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_move_slicer() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::MoveSlicer(
sheets::embedded_object::MoveSlicerCommand {
spreadsheet_id: "sheet-1".to_string(),
slicer_id: 1,
sheet: None,
anchor: Some("F2".to_string()),
offset_x: None,
offset_y: None,
width: None,
height: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_update_chart_border() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UpdateChartBorder(
sheets::embedded_object::UpdateChartBorderCommand {
spreadsheet_id: "sheet-1".to_string(),
chart_id: 1,
color: Some("#4A86E8".to_string()),
clear: false,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_delete_sheet() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DeleteSheet(
sheets::structure::DeleteSheetCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_delete_rows() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DeleteRows(sheets::structure::DeleteRowsCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
at: 5,
count: 3,
write: no_write_args(),
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_delete_columns() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DeleteColumns(
sheets::structure::DeleteColumnsCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
at: 2,
count: 1,
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_delete_range() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DeleteRange(
sheets::structure::DeleteRangeCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
range: sheets::structure::GridRangeArgs {
start_row: 2,
end_row: 4,
start_column: 2,
end_column: 3,
shift: sheets::structure::ShiftArg::Rows,
},
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_duplicate_sheet() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DuplicateSheet(
sheets::structure::DuplicateSheetCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
title: Some("Q2 copy".to_string()),
index: None,
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_reorder_sheet() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ReorderSheet(
sheets::structure::ReorderSheetCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
index: 0,
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_hide_sheet() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::HideSheet(sheets::structure::HideSheetCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
write: no_write_args(),
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_show_sheet() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ShowSheet(sheets::structure::ShowSheetCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
sheet: "Q2".to_string(),
write: no_write_args(),
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_update_workbook_properties() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UpdateWorkbookProperties(
sheets::structure::UpdateWorkbookPropertiesCommand {
target: sheets::structure::SpreadsheetIdArg {
spreadsheet_id: "sheet-1".to_string(),
},
locale: Some("en_US".to_string()),
time_zone: None,
auto_recalc: None,
iterative_calculation: None,
iterative_calculation_max_iterations: None,
iterative_calculation_convergence_threshold: None,
write: no_write_args(),
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_format_cells() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::FormatCells(sheets::format::FormatCellsCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
bold: Some(true),
italic: None,
strikethrough: None,
underline: None,
font_size: None,
text_color: None,
background: None,
horizontal_align: Some(sheets::format::HorizontalAlign::Center),
vertical_align: Some(sheets::format::VerticalAlign::Middle),
number_format: Some("#,##0.00".to_string()),
number_format_type: Some(sheets::format::NumberFormatType::Currency),
wrap: Some(sheets::format::WrapStrategy::Wrap),
font_family: None,
text_rotation_angle: None,
text_rotation_vertical: false,
hyperlink_display_type: None,
padding_top: None,
padding_right: None,
padding_bottom: None,
padding_left: None,
text_direction: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
fn base_format_cells_command() -> sheets::format::FormatCellsCommand {
sheets::format::FormatCellsCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
bold: None,
italic: None,
strikethrough: None,
underline: None,
font_size: None,
text_color: None,
background: None,
horizontal_align: None,
vertical_align: None,
number_format: None,
number_format_type: None,
wrap: None,
font_family: None,
text_rotation_angle: None,
text_rotation_vertical: false,
hyperlink_display_type: None,
padding_top: None,
padding_right: None,
padding_bottom: None,
padding_left: None,
text_direction: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
}
}
#[tokio::test]
async fn dispatch_routes_sheets_format_cells_text_rotation_angle() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::FormatCells(sheets::format::FormatCellsCommand {
text_rotation_angle: Some(45),
..base_format_cells_command()
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_format_cells_text_rotation_vertical() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::FormatCells(sheets::format::FormatCellsCommand {
text_rotation_vertical: true,
..base_format_cells_command()
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_update_borders() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UpdateBorders(
sheets::format::UpdateBordersCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
top: true,
bottom: false,
left: false,
right: false,
all: false,
inner_horizontal: false,
inner_vertical: false,
style: sheets::format::BorderStyle::Dashed,
color: Some("#FF0000".to_string()),
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_merge_cells() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::MergeCells(sheets::format::MergeCellsCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
r#type: sheets::format::MergeType::Rows,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_unmerge_cells() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UnmergeCells(sheets::format::UnmergeCellsCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
}),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_auto_resize_dimension() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::AutoResizeDimension(
sheets::format::AutoResizeDimensionCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q2".to_string(),
dimension: sheets::format::DimensionArg::Columns,
start: 1,
end: 3,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_update_dimension_properties() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UpdateDimensionProperties(
sheets::format::UpdateDimensionPropertiesCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q2".to_string(),
dimension: sheets::format::DimensionArg::Rows,
start: 1,
end: 3,
pixel_size: 42,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_set_data_validation() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::SetDataValidation(Box::new(
sheets::validation::SetDataValidationCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:A10".to_string()),
sheet: None,
one_of_list: Some(vec!["yes".to_string(), "no".to_string()]),
one_of_range: None,
number_between: None,
number_not_between: None,
number_greater: None,
number_greater_eq: None,
number_less: None,
number_less_eq: None,
number_eq: None,
number_not_eq: None,
text_contains: None,
text_not_contains: None,
text_starts_with: None,
text_ends_with: None,
text_eq: None,
date_after: None,
date_before: None,
date_on: None,
date_between: None,
blank: false,
not_blank: false,
checkbox: false,
custom_formula: None,
input_message: Some("Pick one".to_string()),
show_warning: false,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
)),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_clear_data_validation() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ClearDataValidation(
sheets::validation::ClearDataValidationCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:A10".to_string()),
sheet: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_set_developer_metadata() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::SetDeveloperMetadata(
sheets::developer_metadata::SetDeveloperMetadataCommand {
spreadsheet_id: "sheet-1".to_string(),
key: "owner".to_string(),
value: "team-a".to_string(),
sheet: None,
dimension: None,
start: None,
end: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_delete_developer_metadata() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DeleteDeveloperMetadata(
sheets::developer_metadata::DeleteDeveloperMetadataCommand {
spreadsheet_id: "sheet-1".to_string(),
key: "owner".to_string(),
sheet: None,
dimension: None,
start: None,
end: None,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_search_developer_metadata() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::SearchDeveloperMetadata(
sheets::developer_metadata::SearchDeveloperMetadataCommand {
spreadsheet_id: "sheet-1".to_string(),
key: None,
sheet: None,
dimension: None,
start: None,
end: None,
output: OutputFormat::Table,
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_sheets_protect_range() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ProtectRange(
sheets::protection::ProtectRangeCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
whole_sheet: false,
description: Some("locked".to_string()),
warning_only: false,
editors: vec!["alice@example.com".to_string()],
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_update_protection() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UpdateProtection(
sheets::protection::UpdateProtectionCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
whole_sheet: false,
description: Some("still locked".to_string()),
warning_only: Some(true),
add_editors: vec!["bob@example.com".to_string()],
remove_editors: vec!["alice@example.com".to_string()],
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_unprotect_range() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UnprotectRange(
sheets::protection::UnprotectRangeCommand {
spreadsheet_id: "sheet-1".to_string(),
range: Some("A1:B2".to_string()),
sheet: None,
whole_sheet: false,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_list_protections() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ListProtections(
sheets::protection::ListProtectionsCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Table,
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
fn cell_empty_only_rule_args() -> sheets::conditional_format::ConditionalFormatRuleArgs {
sheets::conditional_format::ConditionalFormatRuleArgs {
number_between: None,
number_not_between: None,
number_greater: None,
number_greater_eq: None,
number_less: None,
number_less_eq: None,
number_eq: None,
number_not_eq: None,
text_contains: None,
text_not_contains: None,
text_starts_with: None,
text_ends_with: None,
text_eq: None,
date_after: None,
date_before: None,
date_on: None,
date_between: None,
cell_empty: true,
cell_not_empty: false,
custom_formula: None,
background: None,
text_color: None,
bold: None,
gradient_min_color: None,
gradient_max_color: None,
gradient_mid_color: None,
gradient_mid_type: None,
gradient_mid_value: None,
}
}
#[tokio::test]
async fn dispatch_routes_sheets_add_conditional_format() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::AddConditionalFormat(Box::new(
sheets::conditional_format::AddConditionalFormatCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q1".to_string(),
ranges: vec!["A1:A10".to_string()],
index: None,
rule: cell_empty_only_rule_args(),
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
)),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_update_conditional_format() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UpdateConditionalFormat(Box::new(
sheets::conditional_format::UpdateConditionalFormatCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q1".to_string(),
index: 0,
ranges: vec!["A1:A10".to_string()],
rule: cell_empty_only_rule_args(),
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
)),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_delete_conditional_format() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DeleteConditionalFormat(
sheets::conditional_format::DeleteConditionalFormatCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q1".to_string(),
index: 0,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_list_conditional_formats() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ListConditionalFormats(
sheets::conditional_format::ListConditionalFormatsCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Table,
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_sheets_set_basic_filter() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::SetBasicFilter(
sheets::filter::SetBasicFilterCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q1".to_string(),
range: "A1:D10".to_string(),
sort_by: vec!["0:asc".to_string()],
hide_values: vec!["1:Closed".to_string()],
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_clear_basic_filter() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ClearBasicFilter(
sheets::filter::ClearBasicFilterCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q1".to_string(),
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_add_filter_view() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::AddFilterView(
sheets::filter::AddFilterViewCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: "Q1".to_string(),
range: "A1:D10".to_string(),
title: Some("Open only".to_string()),
sort_by: Vec::new(),
hide_values: Vec::new(),
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_update_filter_view() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::UpdateFilterView(
sheets::filter::UpdateFilterViewCommand {
spreadsheet_id: "sheet-1".to_string(),
filter_view_id: 7,
sheet: None,
range: None,
title: Some("Renamed".to_string()),
sort_by: Vec::new(),
hide_values: Vec::new(),
clear_sort: false,
clear_criteria: false,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_delete_filter_view() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::DeleteFilterView(
sheets::filter::DeleteFilterViewCommand {
spreadsheet_id: "sheet-1".to_string(),
filter_view_id: 7,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
},
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_sheets_list_filter_views() {
let guard = crate::drive::test_support::EnvGuard::take();
guard.redirect_api_hosts_to_a_dead_port();
let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
command: sheets::SheetsSubcommands::ListFilterViews(
sheets::filter::ListFilterViewsCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Table,
},
),
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_search() {
let cmd = DriveSubcommands::Search(search::SearchCommand {
query: "name contains 'x'".to_string(),
limit: 10,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_read() {
let cmd = DriveSubcommands::Read(read::ReadCommand {
file_id: "f1".to_string(),
content: false,
export_mime_type: None,
out_file: None,
verify: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_sync() {
let dir = tempfile::tempdir().unwrap();
let cmd = DriveSubcommands::Sync(sync::SyncCommand {
folder_id: "root".into(),
dest: dir.path().into(),
export_mime_type: None,
verify: false,
dry_run: true,
output: OutputFormat::Json,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0);
}
#[test]
fn sync_parser_requires_destination_and_accepts_options() {
use clap::Parser;
assert!(DriveCommand::try_parse_from(["drive", "sync", "root"]).is_err());
let cmd = DriveCommand::try_parse_from([
"drive",
"sync",
"root",
"--dest",
"mirror",
"--verify",
"--dry-run",
"--export-mime-type",
"application/pdf",
"-o",
"json",
])
.unwrap();
let DriveSubcommands::Sync(sync) = cmd.command else {
panic!("expected sync"); };
assert!(sync.verify && sync.dry_run);
assert_eq!(sync.export_mime_type.as_deref(), Some("application/pdf"));
}
#[tokio::test]
async fn dispatch_routes_dedupe() {
let cmd = DriveSubcommands::Dedupe(dedupe::DedupeCommand {
query: "name contains 'x'".to_string(),
limit: 10,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_create() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveSubcommands::Create(create::CreateCommand {
name: "New File".to_string(),
parent: "parent-1".to_string(),
folder: false,
mime_type: None,
dry_run: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_upload() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let content_dir = tempfile::tempdir().unwrap();
let local_path = content_dir.path().join("upload-me.txt");
std::fs::write(&local_path, b"content").unwrap();
let cmd = DriveSubcommands::Upload(upload::UploadCommand {
local_path,
parent: "parent-1".to_string(),
name: None,
mime_type: None,
dry_run: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_edit() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let content_dir = tempfile::tempdir().unwrap();
let content_path = content_dir.path().join("new-content.txt");
std::fs::write(&content_path, b"content").unwrap();
let cmd = DriveSubcommands::Edit(edit::EditCommand {
file_id: "f1".to_string(),
content: content_path.to_str().unwrap().to_string(),
mime_type: None,
dry_run: false,
lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_ok());
}
#[tokio::test]
async fn dispatch_routes_rename() {
let cmd = DriveSubcommands::Rename(rename::RenameCommand {
file_id: "f1".to_string(),
new_name: "New Name".to_string(),
dry_run: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_move() {
let cmd = DriveSubcommands::Move(move_file::MoveCommand {
file_ids: vec!["f1".to_string()],
to: "dest1".to_string(),
allow_visibility_increase: false,
allow_visibility_decrease: false,
allow_drive_boundary_crossing: false,
dry_run: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
}