1pub(crate) mod account;
4pub(crate) mod auth;
5pub(crate) mod create;
6pub(crate) mod dedupe;
7pub(crate) mod docs;
8pub(crate) mod edit;
9pub(crate) mod format;
10pub(crate) mod helpers;
11pub(crate) mod lease;
12pub(crate) mod move_file;
15pub(crate) mod permissions;
16pub(crate) mod read;
17pub(crate) mod rename;
18pub(crate) mod search;
19pub(crate) mod sheets;
20pub(crate) mod slides;
21pub(crate) mod sync;
22pub(crate) mod trash;
23pub(crate) mod upload;
24
25use anyhow::Result;
26use clap::{Parser, Subcommand};
27
28use crate::drive::account::DRIVE_ACCOUNT_ENV;
29use crate::drive::client::DriveClient;
30
31#[derive(Parser)]
33pub struct DriveCommand {
34 #[arg(long, global = true, value_name = "NAME")]
46 pub account: Option<String>,
47 #[command(subcommand)]
49 pub command: DriveSubcommands,
50}
51
52#[derive(Subcommand)]
54pub enum DriveSubcommands {
55 Auth(auth::AuthCommand),
57 Account(account::AccountCommand),
59 Search(search::SearchCommand),
61 Read(read::ReadCommand),
63 Dedupe(dedupe::DedupeCommand),
65 Sync(sync::SyncCommand),
67 Create(create::CreateCommand),
71 Upload(upload::UploadCommand),
75 Edit(edit::EditCommand),
81 Trash(trash::TrashCommand),
84 Untrash(trash::TrashCommand),
87 Lease(lease::LeaseCommand),
91 Rename(rename::RenameCommand),
94 Move(move_file::MoveCommand),
97 Permissions(permissions::PermissionsCommand),
102 Docs(docs::DocsCommand),
105 Slides(slides::SlidesCommand),
107 Sheets(sheets::SheetsCommand),
110}
111
112impl DriveCommand {
113 pub async fn execute(self) -> Result<()> {
120 let _account_guard = self
129 .account
130 .as_ref()
131 .map(|account| crate::utils::env::ScopedEnvVar::set(DRIVE_ACCOUNT_ENV, account));
132
133 match self.command {
134 DriveSubcommands::Auth(cmd) => cmd.execute().await,
135 DriveSubcommands::Account(cmd) => cmd.execute(),
136 DriveSubcommands::Permissions(cmd) => cmd.execute().await,
141 DriveSubcommands::Lease(cmd) => cmd.execute(helpers::create_client).await,
152 command => {
153 let client = helpers::create_client()?;
154 command.dispatch(&client).await
155 }
156 }
157 }
158}
159
160impl DriveSubcommands {
161 async fn dispatch(self, client: &DriveClient) -> Result<()> {
165 match self {
166 Self::Auth(_) => unreachable!("Auth is dispatched before client resolution"),
167 Self::Account(_) => unreachable!("Account is dispatched before client resolution"),
168 Self::Permissions(_) => {
169 unreachable!("Permissions is dispatched before client resolution")
170 }
171 Self::Lease(_) => unreachable!("Lease is dispatched before client resolution"),
172 Self::Search(cmd) => cmd.execute(client).await,
173 Self::Read(cmd) => cmd.execute(client).await,
174 Self::Dedupe(cmd) => cmd.execute(client).await,
175 Self::Sync(cmd) => cmd.execute(client).await,
176 Self::Create(cmd) => cmd.execute(client).await,
177 Self::Upload(cmd) => cmd.execute(client).await,
178 Self::Edit(cmd) => cmd.execute(client).await,
179 Self::Trash(cmd) => cmd.execute(client, false).await,
180 Self::Untrash(cmd) => cmd.execute(client, true).await,
181 Self::Rename(cmd) => cmd.execute(client).await,
182 Self::Move(cmd) => cmd.execute(client).await,
183 Self::Docs(cmd) => cmd.execute(client).await,
184 Self::Slides(cmd) => cmd.execute(client).await,
185 Self::Sheets(cmd) => cmd.execute(client).await,
186 }
187 }
188}
189
190#[cfg(test)]
191#[allow(clippy::unwrap_used)]
192mod tests {
193 use super::*;
194 use crate::cli::drive::format::OutputFormat;
195 use crate::drive::auth::{DriveCredentials, DriveGrantedScopes};
196 use crate::utils::secret::Secret;
197
198 fn dead_credentials() -> DriveCredentials {
199 DriveCredentials {
200 client_id: "client".to_string(),
201 client_secret: Secret::new("secret"),
202 refresh_token: Secret::new("refresh"),
203 scope: DriveGrantedScopes::READONLY,
204 }
205 }
206
207 fn dead_client() -> DriveClient {
208 DriveClient::new("http://127.0.0.1:1", &dead_credentials()).unwrap()
209 }
210
211 fn no_write_args() -> helpers::StructureWriteArgs {
214 helpers::StructureWriteArgs {
215 dry_run: false,
216 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
217 output: OutputFormat::Table,
218 }
219 }
220
221 #[tokio::test]
222 async fn execute_routes_auth_subcommand_and_surfaces_missing_credentials() {
223 let guard = crate::drive::test_support::EnvGuard::take();
224 let _dir = guard.clear_credentials();
225
226 let cmd = DriveCommand {
227 account: None,
228 command: DriveSubcommands::Auth(auth::AuthCommand {
229 command: auth::AuthSubcommands::Status(auth::StatusCommand { all: false }),
230 }),
231 };
232 let err = cmd.execute().await.unwrap_err();
233 assert!(err.to_string().contains("not configured"));
234 }
235
236 #[tokio::test]
237 async fn execute_non_auth_subcommand_errors_when_credentials_missing() {
238 let guard = crate::drive::test_support::EnvGuard::take();
239 let _dir = guard.clear_credentials();
240
241 let cmd = DriveCommand {
242 account: None,
243 command: DriveSubcommands::Search(search::SearchCommand {
244 query: "name contains 'report'".to_string(),
245 limit: 10,
246 output: OutputFormat::Table,
247 }),
248 };
249 let err = cmd.execute().await.unwrap_err();
250 assert!(err.to_string().contains("not configured"));
251 }
252
253 #[tokio::test]
265 async fn execute_lease_release_needs_no_credentials() {
266 let guard = crate::drive::test_support::EnvGuard::take();
267 let dir = guard.clear_credentials();
268 let audit = crate::test_support::AuditLogGuard::redirect(dir.path());
269
270 let cmd = lease::LeaseCommand::release_for_test("no-such-token".to_string());
271 let code = cmd
272 .run(|| panic!("release must never resolve a client"))
273 .await
274 .unwrap();
275 assert_eq!(code, 1, "NoSuchToken must be a non-zero exit");
276
277 assert_eq!(audit.verdicts(), vec!["release-no-such-token".to_string()]);
278 }
279
280 #[tokio::test]
284 async fn execute_lease_prune_dry_run_needs_no_credentials() {
285 let guard = crate::drive::test_support::EnvGuard::take();
286 let _dir = guard.clear_credentials();
287
288 let cmd = DriveCommand {
289 account: None,
290 command: DriveSubcommands::Lease(lease::LeaseCommand::prune_for_test("1h", true)),
291 };
292 cmd.execute().await.unwrap();
293 }
294
295 #[tokio::test]
298 async fn execute_lease_acquire_still_errors_when_credentials_missing() {
299 let guard = crate::drive::test_support::EnvGuard::take();
300 let _dir = guard.clear_credentials();
301
302 let cmd = DriveCommand {
303 account: None,
304 command: DriveSubcommands::Lease(lease::LeaseCommand::acquire_for_test(
305 "file-1".to_string(),
306 )),
307 };
308 let err = cmd.execute().await.unwrap_err();
309 assert!(err.to_string().contains("not configured"), "{err}");
310 }
311
312 #[tokio::test]
313 async fn execute_restores_account_env_var_after_return() {
314 let guard = crate::drive::test_support::EnvGuard::take();
315 let _dir = guard.clear_credentials();
316
317 let cmd = DriveCommand {
318 account: Some("work".to_string()),
319 command: DriveSubcommands::Account(account::AccountCommand {
320 command: account::AccountSubcommands::List(account::list::ListCommand {
321 output: OutputFormat::Table,
322 }),
323 }),
324 };
325 cmd.execute().await.unwrap();
326 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
327 }
328
329 #[tokio::test]
330 async fn execute_restores_previous_account_env_var_after_return() {
331 let guard = crate::drive::test_support::EnvGuard::take();
332 let _dir = guard.clear_credentials();
333 std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
334
335 let cmd = DriveCommand {
336 account: Some("work".to_string()),
337 command: DriveSubcommands::Account(account::AccountCommand {
338 command: account::AccountSubcommands::List(account::list::ListCommand {
339 output: OutputFormat::Table,
340 }),
341 }),
342 };
343 cmd.execute().await.unwrap();
344 assert_eq!(
345 std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
346 Some("personal")
347 );
348 }
349
350 #[tokio::test]
351 async fn execute_does_not_leak_account_across_sequential_calls() {
352 let guard = crate::drive::test_support::EnvGuard::take();
353 let _dir = guard.clear_credentials();
354
355 let account_list_cmd = || DriveCommand {
356 account: None,
357 command: DriveSubcommands::Account(account::AccountCommand {
358 command: account::AccountSubcommands::List(account::list::ListCommand {
359 output: OutputFormat::Table,
360 }),
361 }),
362 };
363
364 let first = DriveCommand {
365 account: Some("alpha".to_string()),
366 ..account_list_cmd()
367 };
368 first.execute().await.unwrap();
369 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
370
371 account_list_cmd().execute().await.unwrap();
374 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
375 }
376
377 #[tokio::test]
378 async fn execute_absent_account_leaves_ambient_env_var_untouched() {
379 let guard = crate::drive::test_support::EnvGuard::take();
380 let _dir = guard.clear_credentials();
381 std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
382
383 let cmd = DriveCommand {
384 account: None,
385 command: DriveSubcommands::Account(account::AccountCommand {
386 command: account::AccountSubcommands::List(account::list::ListCommand {
387 output: OutputFormat::Table,
388 }),
389 }),
390 };
391 cmd.execute().await.unwrap();
392 assert_eq!(
393 std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
394 Some("personal")
395 );
396 }
397
398 #[tokio::test]
399 async fn execute_routes_account_list_without_client_resolution() {
400 let guard = crate::drive::test_support::EnvGuard::take();
401 let _dir = guard.clear_credentials();
402
403 let cmd = DriveCommand {
404 account: None,
405 command: DriveSubcommands::Account(account::AccountCommand {
406 command: account::AccountSubcommands::List(account::list::ListCommand {
407 output: OutputFormat::Table,
408 }),
409 }),
410 };
411 cmd.execute().await.unwrap();
412 }
413
414 #[tokio::test]
415 async fn execute_routes_permissions_show_without_client_resolution() {
416 let guard = crate::drive::test_support::EnvGuard::take();
417 let _dir = guard.clear_credentials();
418
419 let cmd = DriveCommand {
420 account: None,
421 command: DriveSubcommands::Permissions(permissions::PermissionsCommand {
422 command: permissions::PermissionsSubcommands::Show(
423 permissions::show::ShowCommand {
424 output: OutputFormat::Table,
425 },
426 ),
427 }),
428 };
429 cmd.execute().await.unwrap();
430 }
431
432 #[tokio::test]
433 async fn execute_routes_permissions_check_and_surfaces_missing_credentials() {
434 let guard = crate::drive::test_support::EnvGuard::take();
435 let _dir = guard.clear_credentials();
436
437 let cmd = DriveCommand {
438 account: None,
439 command: DriveSubcommands::Permissions(permissions::PermissionsCommand {
440 command: permissions::PermissionsSubcommands::Check(
441 permissions::check::CheckCommand {
442 id: "f1".to_string(),
443 operation: permissions::check::OperationArg::Read,
444 output: OutputFormat::Table,
445 },
446 ),
447 }),
448 };
449 let err = cmd.execute().await.unwrap_err();
450 assert!(err.to_string().contains("not configured"));
451 }
452
453 #[tokio::test]
454 async fn dispatch_routes_slides_read_verbs_to_the_slides_host() {
455 let guard = crate::drive::test_support::EnvGuard::take();
456 guard.redirect_api_hosts_to_a_dead_port();
457 for command in [
458 slides::SlidesSubcommands::Info(slides::info::InfoCommand {
459 presentation_id: "p".into(),
460 output: OutputFormat::Table,
461 }),
462 slides::SlidesSubcommands::Read(slides::read::ReadCommand {
463 presentation_id: "p".into(),
464 slides: vec![],
465 output: OutputFormat::Table,
466 }),
467 ] {
468 let cmd = DriveSubcommands::Slides(slides::SlidesCommand { command });
469 assert!(cmd.dispatch(&dead_client()).await.is_err());
470 }
471 }
472
473 #[tokio::test]
474 async fn dispatch_routes_docs_info() {
475 let guard = crate::drive::test_support::EnvGuard::take();
480 guard.redirect_api_hosts_to_a_dead_port();
481
482 let cmd = DriveSubcommands::Docs(docs::DocsCommand {
483 command: docs::DocsSubcommands::Info(docs::info::InfoCommand {
484 document_id: "d1".to_string(),
485 output: OutputFormat::Table,
486 }),
487 });
488 assert!(cmd.dispatch(&dead_client()).await.is_err());
489 }
490
491 #[tokio::test]
492 async fn dispatch_routes_docs_read() {
493 let guard = crate::drive::test_support::EnvGuard::take();
494 guard.redirect_api_hosts_to_a_dead_port();
495
496 let cmd = DriveSubcommands::Docs(docs::DocsCommand {
497 command: docs::DocsSubcommands::Read(docs::read::ReadCommand {
498 document_id: "d1".to_string(),
499 tab: None,
500 suggestions_view: docs::read::SuggestionsViewArg::Default,
501 output: OutputFormat::Table,
502 }),
503 });
504 assert!(cmd.dispatch(&dead_client()).await.is_err());
505 }
506
507 #[tokio::test]
519 async fn dispatch_routes_docs_replace() {
520 let guard = crate::drive::test_support::EnvGuard::take();
521 guard.redirect_api_hosts_to_a_dead_port();
522 let _dir = guard.clear_credentials();
523
524 let cmd = DriveSubcommands::Docs(docs::DocsCommand {
525 command: docs::DocsSubcommands::Replace(docs::write::ReplaceCommand {
526 document_id: "d1".to_string(),
527 search: "a".to_string(),
528 replace: "b".to_string(),
529 ignore_case: false,
530 dry_run: false,
531 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
532 output: OutputFormat::Table,
533 }),
534 });
535 assert!(cmd.dispatch(&dead_client()).await.is_ok());
536 }
537
538 #[tokio::test]
539 async fn dispatch_routes_docs_append() {
540 let guard = crate::drive::test_support::EnvGuard::take();
541 guard.redirect_api_hosts_to_a_dead_port();
542
543 let _dir = guard.clear_credentials();
544
545 let cmd = DriveSubcommands::Docs(docs::DocsCommand {
546 command: docs::DocsSubcommands::Append(docs::write::AppendCommand {
547 document_id: "d1".to_string(),
548 text: Some("x".to_string()),
549 text_file: None,
550 dry_run: false,
551 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
552 output: OutputFormat::Table,
553 }),
554 });
555 assert!(cmd.dispatch(&dead_client()).await.is_ok());
557 }
558
559 fn insert_command(
560 before: Option<&str>,
561 after: Option<&str>,
562 text: Option<&str>,
563 text_file: Option<&str>,
564 ) -> DriveSubcommands {
565 DriveSubcommands::Docs(docs::DocsCommand {
566 command: docs::DocsSubcommands::Insert(docs::write::InsertCommand {
567 segment_id: None,
568 tab_id: None,
569 document_id: "d1".to_string(),
570 before: before.map(str::to_string),
571 after: after.map(str::to_string),
572 text: text.map(str::to_string),
573 text_file: text_file.map(str::to_string),
574 ignore_case: true,
575 dry_run: false,
576 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
577 output: OutputFormat::Table,
578 }),
579 })
580 }
581
582 fn delete_command(
583 match_text: Option<&str>,
584 from: Option<&str>,
585 to: Option<&str>,
586 ) -> DriveSubcommands {
587 DriveSubcommands::Docs(docs::DocsCommand {
588 command: docs::DocsSubcommands::Delete(docs::write::DeleteCommand {
589 segment_id: None,
590 tab_id: None,
591 document_id: "d1".to_string(),
592 match_text: match_text.map(str::to_string),
593 from: from.map(str::to_string),
594 to: to.map(str::to_string),
595 ignore_case: true,
596 dry_run: false,
597 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
598 output: OutputFormat::Table,
599 }),
600 })
601 }
602
603 #[tokio::test]
608 async fn dispatch_routes_docs_insert_for_every_anchor_side_and_text_source() {
609 let guard = crate::drive::test_support::EnvGuard::take();
610 let dir = guard.clear_credentials();
611 guard.redirect_api_hosts_to_a_dead_port();
612 let file = dir.path().join("insert.txt");
613 std::fs::write(&file, "from a file").unwrap();
614 let file = file.to_str().unwrap();
615
616 for cmd in [
617 insert_command(Some("a"), None, Some("x"), None),
618 insert_command(None, Some("a"), Some("x"), None),
619 insert_command(Some("a"), None, None, Some(file)),
620 insert_command(None, Some("a"), None, Some(file)),
621 ] {
622 assert!(cmd.dispatch(&dead_client()).await.is_ok());
623 }
624 }
625
626 #[tokio::test]
630 async fn dispatch_refuses_an_insert_without_exactly_one_anchor_and_text_source() {
631 let guard = crate::drive::test_support::EnvGuard::take();
632 let _dir = guard.clear_credentials();
633 guard.redirect_api_hosts_to_a_dead_port();
634
635 for cmd in [
636 insert_command(None, None, Some("x"), None),
637 insert_command(Some("a"), Some("b"), Some("x"), None),
638 ] {
639 let err = cmd.dispatch(&dead_client()).await.unwrap_err();
640 assert!(err.to_string().contains("--before or --after"), "{err}");
641 }
642 for cmd in [
643 insert_command(Some("a"), None, None, None),
644 insert_command(Some("a"), None, Some("x"), Some("f")),
645 ] {
646 let err = cmd.dispatch(&dead_client()).await.unwrap_err();
647 assert!(err.to_string().contains("--text or --text-file"), "{err}");
648 }
649 }
650
651 #[tokio::test]
652 async fn dispatch_routes_docs_delete_for_a_match_and_for_a_range() {
653 let guard = crate::drive::test_support::EnvGuard::take();
654 let _dir = guard.clear_credentials();
655 guard.redirect_api_hosts_to_a_dead_port();
656
657 for cmd in [
658 delete_command(Some("a"), None, None),
659 delete_command(None, Some("a"), Some("b")),
660 ] {
661 assert!(cmd.dispatch(&dead_client()).await.is_ok());
662 }
663 }
664
665 #[tokio::test]
666 async fn dispatch_refuses_a_delete_that_is_not_a_match_or_a_full_range() {
667 let guard = crate::drive::test_support::EnvGuard::take();
668 let _dir = guard.clear_credentials();
669 guard.redirect_api_hosts_to_a_dead_port();
670
671 for cmd in [
672 delete_command(None, None, None),
673 delete_command(None, Some("a"), None),
674 delete_command(None, None, Some("b")),
675 delete_command(Some("a"), Some("a"), Some("b")),
676 delete_command(Some("a"), Some("a"), None),
677 ] {
678 let err = cmd.dispatch(&dead_client()).await.unwrap_err();
679 assert!(
680 err.to_string().contains("--match or both --from and --to"),
681 "{err}"
682 );
683 }
684 }
685
686 fn list_selection(
687 match_text: Option<&str>,
688 from: Option<&str>,
689 to: Option<&str>,
690 ) -> docs::write::ListSelection {
691 docs::write::ListSelection {
692 document_id: "d1".to_string(),
693 match_text: match_text.map(str::to_string),
694 from: from.map(str::to_string),
695 to: to.map(str::to_string),
696 ignore_case: true,
697 dry_run: false,
698 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
699 output: OutputFormat::Table,
700 }
701 }
702
703 fn create_bullets_command(selection: docs::write::ListSelection) -> DriveSubcommands {
704 DriveSubcommands::Docs(docs::DocsCommand {
705 command: docs::DocsSubcommands::CreateBullets(docs::write::CreateBulletsCommand {
706 selection,
707 preset: docs::write::BulletPresetArg::BulletCheckbox,
708 }),
709 })
710 }
711
712 fn delete_bullets_command(selection: docs::write::ListSelection) -> DriveSubcommands {
713 DriveSubcommands::Docs(docs::DocsCommand {
714 command: docs::DocsSubcommands::DeleteBullets(docs::write::DeleteBulletsCommand {
715 selection,
716 }),
717 })
718 }
719
720 #[tokio::test]
725 async fn dispatch_routes_docs_create_and_delete_bullets() {
726 let guard = crate::drive::test_support::EnvGuard::take();
727 let _dir = guard.clear_credentials();
728 guard.redirect_api_hosts_to_a_dead_port();
729
730 for cmd in [
731 create_bullets_command(list_selection(Some("a"), None, None)),
732 create_bullets_command(list_selection(None, Some("a"), Some("b"))),
733 delete_bullets_command(list_selection(Some("a"), None, None)),
734 delete_bullets_command(list_selection(None, Some("a"), Some("b"))),
735 ] {
736 assert!(cmd.dispatch(&dead_client()).await.is_ok());
737 }
738 }
739
740 #[tokio::test]
744 async fn dispatch_refuses_a_list_verb_that_is_not_a_match_or_a_full_range() {
745 let guard = crate::drive::test_support::EnvGuard::take();
746 let _dir = guard.clear_credentials();
747 guard.redirect_api_hosts_to_a_dead_port();
748
749 for (match_text, from, to) in [
750 (None, None, None),
751 (None, Some("a"), None),
752 (None, None, Some("b")),
753 (Some("a"), Some("a"), Some("b")),
754 (Some("a"), Some("a"), None),
755 ] {
756 for cmd in [
757 create_bullets_command(list_selection(match_text, from, to)),
758 delete_bullets_command(list_selection(match_text, from, to)),
759 ] {
760 let err = cmd.dispatch(&dead_client()).await.unwrap_err();
761 assert!(
762 err.to_string().contains("--match or both --from and --to"),
763 "{err}"
764 );
765 }
766 }
767 }
768
769 fn style_selection(
770 match_text: Option<&str>,
771 from: Option<&str>,
772 to: Option<&str>,
773 ) -> docs::style::Selection {
774 docs::style::Selection {
775 document_id: "d1".to_string(),
776 match_text: match_text.map(str::to_string),
777 from: from.map(str::to_string),
778 to: to.map(str::to_string),
779 ignore_case: true,
780 dry_run: false,
781 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
782 output: OutputFormat::Table,
783 }
784 }
785
786 fn text_style_command(
787 match_text: Option<&str>,
788 from: Option<&str>,
789 to: Option<&str>,
790 ) -> DriveSubcommands {
791 DriveSubcommands::Docs(docs::DocsCommand {
792 command: docs::DocsSubcommands::TextStyle(docs::style::TextStyleCommand {
793 selection: style_selection(match_text, from, to),
794 bold: Some(true),
795 italic: None,
796 underline: None,
797 strikethrough: None,
798 }),
799 })
800 }
801
802 fn paragraph_style_command(
803 match_text: Option<&str>,
804 from: Option<&str>,
805 to: Option<&str>,
806 ) -> DriveSubcommands {
807 use crate::drive::docs::style::{Alignment, NamedStyle};
808 DriveSubcommands::Docs(docs::DocsCommand {
809 command: docs::DocsSubcommands::ParagraphStyle(docs::style::ParagraphStyleCommand {
810 selection: style_selection(match_text, from, to),
811 alignment: Some(Alignment::Center),
812 named_style: Some(NamedStyle::Title),
813 }),
814 })
815 }
816
817 #[tokio::test]
822 async fn dispatch_routes_docs_text_style_and_paragraph_style_for_a_match_and_for_a_range() {
823 let guard = crate::drive::test_support::EnvGuard::take();
824 let _dir = guard.clear_credentials();
825 guard.redirect_api_hosts_to_a_dead_port();
826
827 for cmd in [
828 text_style_command(Some("a"), None, None),
829 text_style_command(None, Some("a"), Some("b")),
830 paragraph_style_command(Some("a"), None, None),
831 paragraph_style_command(None, Some("a"), Some("b")),
832 ] {
833 assert!(cmd.dispatch(&dead_client()).await.is_ok());
834 }
835 }
836
837 #[tokio::test]
840 async fn dispatch_refuses_a_format_that_is_not_a_match_or_a_full_range() {
841 let guard = crate::drive::test_support::EnvGuard::take();
842 let _dir = guard.clear_credentials();
843 guard.redirect_api_hosts_to_a_dead_port();
844
845 for (match_text, from, to) in [
846 (None, None, None),
847 (None, Some("a"), None),
848 (None, None, Some("b")),
849 (Some("a"), Some("a"), Some("b")),
850 (Some("a"), Some("a"), None),
851 ] {
852 for cmd in [
853 text_style_command(match_text, from, to),
854 paragraph_style_command(match_text, from, to),
855 ] {
856 let err = cmd.dispatch(&dead_client()).await.unwrap_err();
857 assert!(
858 err.to_string().contains("--match or both --from and --to"),
859 "{err}"
860 );
861 }
862 }
863 }
864
865 fn table_common() -> docs::table::Common {
866 docs::table::Common {
867 document_id: "d1".to_string(),
868 dry_run: false,
869 ignore_case: true,
870 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
871 output: OutputFormat::Table,
872 }
873 }
874
875 fn insert_table_command(before: Option<&str>, after: Option<&str>) -> DriveSubcommands {
876 DriveSubcommands::Docs(docs::DocsCommand {
877 command: docs::DocsSubcommands::InsertTable(docs::table::InsertTableCommand {
878 common: table_common(),
879 before: before.map(str::to_string),
880 after: after.map(str::to_string),
881 rows: 2,
882 columns: 3,
883 }),
884 })
885 }
886
887 fn insert_dimension(before: bool, after: bool) -> docs::table::InsertDimensionCommand {
888 docs::table::InsertDimensionCommand {
889 common: table_common(),
890 cell: "a".to_string(),
891 before,
892 after,
893 }
894 }
895
896 fn delete_dimension() -> docs::table::DeleteDimensionCommand {
897 docs::table::DeleteDimensionCommand {
898 common: table_common(),
899 cell: "a".to_string(),
900 }
901 }
902
903 #[tokio::test]
908 async fn dispatch_routes_every_docs_table_verb() {
909 let guard = crate::drive::test_support::EnvGuard::take();
910 let _dir = guard.clear_credentials();
911 guard.redirect_api_hosts_to_a_dead_port();
912
913 let wrap = |command| DriveSubcommands::Docs(docs::DocsCommand { command });
914 for cmd in [
915 insert_table_command(Some("a"), None),
916 insert_table_command(None, Some("a")),
917 wrap(docs::DocsSubcommands::InsertTableRow(insert_dimension(
918 true, false,
919 ))),
920 wrap(docs::DocsSubcommands::InsertTableRow(insert_dimension(
921 false, true,
922 ))),
923 wrap(docs::DocsSubcommands::InsertTableColumn(insert_dimension(
924 true, false,
925 ))),
926 wrap(docs::DocsSubcommands::InsertTableColumn(insert_dimension(
927 false, true,
928 ))),
929 wrap(docs::DocsSubcommands::DeleteTableRow(delete_dimension())),
930 wrap(docs::DocsSubcommands::DeleteTableColumn(delete_dimension())),
931 ] {
932 assert!(cmd.dispatch(&dead_client()).await.is_ok());
933 }
934 }
935
936 #[tokio::test]
940 async fn dispatch_refuses_an_insert_table_without_exactly_one_anchor_side() {
941 let guard = crate::drive::test_support::EnvGuard::take();
942 let _dir = guard.clear_credentials();
943 guard.redirect_api_hosts_to_a_dead_port();
944
945 for (before, after) in [(None, None), (Some("a"), Some("b"))] {
946 let err = insert_table_command(before, after)
947 .dispatch(&dead_client())
948 .await
949 .unwrap_err();
950 assert!(
951 err.to_string()
952 .contains("exactly one of --before or --after"),
953 "{err}"
954 );
955 }
956 }
957
958 fn scoped_args() -> docs::named_range::ScopedArgs {
959 docs::named_range::ScopedArgs {
960 document_id: "d1".to_string(),
961 tab: "legacy".to_string(),
962 segment: "body".to_string(),
963 dry_run: false,
964 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
965 output: OutputFormat::Table,
966 }
967 }
968
969 fn named_range_command(command: docs::DocsSubcommands) -> DriveSubcommands {
970 DriveSubcommands::Docs(docs::DocsCommand { command })
971 }
972
973 fn replace_named_range_content(
974 text: Option<&str>,
975 text_file: Option<&str>,
976 ) -> DriveSubcommands {
977 named_range_command(docs::DocsSubcommands::ReplaceNamedRangeContent(
978 docs::named_range::ReplaceNamedRangeContentCommand {
979 scope: scoped_args(),
980 id: "nr".to_string(),
981 text: text.map(str::to_string),
982 text_file: text_file.map(str::to_string),
983 },
984 ))
985 }
986
987 #[tokio::test]
991 async fn dispatch_routes_docs_named_range_verbs() {
992 let guard = crate::drive::test_support::EnvGuard::take();
993 let _dir = guard.clear_credentials();
994 guard.redirect_api_hosts_to_a_dead_port();
995 let file = tempfile::NamedTempFile::new().unwrap();
996 std::fs::write(file.path(), "from a file").unwrap();
997
998 for cmd in [
999 named_range_command(docs::DocsSubcommands::CreateNamedRange(
1000 docs::named_range::CreateNamedRangeCommand {
1001 scope: scoped_args(),
1002 name: "label".to_string(),
1003 start_index: 1,
1004 end_index: 3,
1005 },
1006 )),
1007 named_range_command(docs::DocsSubcommands::DeleteNamedRange(
1008 docs::named_range::DeleteNamedRangeCommand {
1009 scope: scoped_args(),
1010 id: "nr".to_string(),
1011 },
1012 )),
1013 replace_named_range_content(Some("inline"), None),
1014 replace_named_range_content(None, Some(file.path().to_str().unwrap())),
1015 ] {
1016 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1017 }
1018 }
1019
1020 #[tokio::test]
1025 async fn dispatch_refuses_named_range_replacement_without_exactly_one_text_source() {
1026 let guard = crate::drive::test_support::EnvGuard::take();
1027 let _dir = guard.clear_credentials();
1028 guard.redirect_api_hosts_to_a_dead_port();
1029
1030 for (text, text_file) in [(None, None), (Some("x"), Some("f"))] {
1031 let err = replace_named_range_content(text, text_file)
1032 .dispatch(&dead_client())
1033 .await
1034 .unwrap_err();
1035 assert!(
1036 err.to_string()
1037 .contains("exactly one of --text or --text-file"),
1038 "{err}"
1039 );
1040 }
1041
1042 let err = replace_named_range_content(None, Some("/nonexistent/no-such-file.txt"))
1043 .dispatch(&dead_client())
1044 .await
1045 .unwrap_err();
1046 assert!(err.to_string().contains("Failed to stat"), "{err}");
1047 }
1048
1049 #[tokio::test]
1050 async fn dispatch_routes_docs_create() {
1051 let guard = crate::drive::test_support::EnvGuard::take();
1052 guard.redirect_api_hosts_to_a_dead_port();
1053
1054 let _dir = guard.clear_credentials();
1055
1056 let cmd = DriveSubcommands::Docs(docs::DocsCommand {
1057 command: docs::DocsSubcommands::Create(docs::create::CreateCommand {
1058 name: "n".to_string(),
1059 parent: "folder-1".to_string(),
1060 text: None,
1061 text_file: None,
1062 dry_run: false,
1063 output: OutputFormat::Table,
1064 }),
1065 });
1066 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1068 }
1069
1070 #[tokio::test]
1076 async fn dispatch_routes_trash_and_untrash() {
1077 let guard = crate::drive::test_support::EnvGuard::take();
1078 let _dir = guard.clear_credentials();
1079
1080 let leaf = || trash::TrashCommand {
1081 file_id: "file-1".to_string(),
1082 dry_run: true,
1083 output: OutputFormat::Table,
1084 };
1085 assert!(DriveSubcommands::Trash(leaf())
1086 .dispatch(&dead_client())
1087 .await
1088 .is_ok());
1089 assert!(DriveSubcommands::Untrash(leaf())
1090 .dispatch(&dead_client())
1091 .await
1092 .is_ok());
1093 }
1094
1095 #[tokio::test]
1096 async fn dispatch_routes_sheets_info() {
1097 let guard = crate::drive::test_support::EnvGuard::take();
1102 guard.redirect_api_hosts_to_a_dead_port();
1103
1104 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1105 command: sheets::SheetsSubcommands::Info(sheets::info::InfoCommand {
1106 spreadsheet_id: "s1".to_string(),
1107 output: OutputFormat::Table,
1108 }),
1109 });
1110 assert!(cmd.dispatch(&dead_client()).await.is_err());
1111 }
1112
1113 #[tokio::test]
1114 async fn dispatch_routes_sheets_read() {
1115 let guard = crate::drive::test_support::EnvGuard::take();
1116 guard.redirect_api_hosts_to_a_dead_port();
1117
1118 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1119 command: sheets::SheetsSubcommands::Read(sheets::read::ReadCommand {
1120 spreadsheet_id: "s1".to_string(),
1121 range: None,
1122 sheet: None,
1123 render: sheets::read::RenderArg::Formatted,
1124 output: OutputFormat::Table,
1125 }),
1126 });
1127 assert!(cmd.dispatch(&dead_client()).await.is_err());
1128 }
1129
1130 #[tokio::test]
1131 async fn dispatch_routes_sheets_write() {
1132 let guard = crate::drive::test_support::EnvGuard::take();
1133 guard.redirect_api_hosts_to_a_dead_port();
1134 let _dir = guard.clear_credentials();
1135
1136 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1137 command: sheets::SheetsSubcommands::Write(sheets::write::WriteCommand {
1138 spreadsheet_id: "s1".to_string(),
1139 range: Some("A1".to_string()),
1140 sheet: None,
1141 values: "/definitely/not/here.csv".to_string(),
1142 values_format: sheets::values::ValuesFormat::Auto,
1143 input: sheets::write::InputArg::UserEntered,
1144 dry_run: false,
1145 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1146 output: OutputFormat::Table,
1147 }),
1148 });
1149 assert!(cmd.dispatch(&dead_client()).await.is_err());
1150 }
1151
1152 #[tokio::test]
1153 async fn dispatch_routes_sheets_append() {
1154 let guard = crate::drive::test_support::EnvGuard::take();
1155 guard.redirect_api_hosts_to_a_dead_port();
1156 let _dir = guard.clear_credentials();
1157
1158 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1159 command: sheets::SheetsSubcommands::Append(sheets::write::AppendCommand {
1160 spreadsheet_id: "s1".to_string(),
1161 range: Some("A1".to_string()),
1162 sheet: None,
1163 values: "/definitely/not/here.csv".to_string(),
1164 values_format: sheets::values::ValuesFormat::Auto,
1165 input: sheets::write::InputArg::UserEntered,
1166 dry_run: false,
1167 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1168 output: OutputFormat::Table,
1169 }),
1170 });
1171 assert!(cmd.dispatch(&dead_client()).await.is_err());
1172 }
1173
1174 #[tokio::test]
1175 async fn dispatch_routes_sheets_clear() {
1176 let guard = crate::drive::test_support::EnvGuard::take();
1177 guard.redirect_api_hosts_to_a_dead_port();
1178 let _dir = guard.clear_credentials();
1179
1180 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1181 command: sheets::SheetsSubcommands::Clear(sheets::write::ClearCommand {
1182 spreadsheet_id: "s1".to_string(),
1183 range: Some("A1".to_string()),
1184 sheet: None,
1185 dry_run: false,
1186 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1187 output: OutputFormat::Table,
1188 }),
1189 });
1190 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1197 }
1198
1199 #[tokio::test]
1200 async fn dispatch_routes_sheets_create() {
1201 let guard = crate::drive::test_support::EnvGuard::take();
1202 guard.redirect_api_hosts_to_a_dead_port();
1203 let _dir = guard.clear_credentials();
1204
1205 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1206 command: sheets::SheetsSubcommands::Create(sheets::create::CreateCommand {
1207 name: "Budget".to_string(),
1208 parent: "parent-1".to_string(),
1209 values: None,
1210 values_format: sheets::values::ValuesFormat::Auto,
1211 input: sheets::write::InputArg::UserEntered,
1212 dry_run: false,
1213 output: OutputFormat::Table,
1214 }),
1215 });
1216 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1220 }
1221
1222 #[tokio::test]
1223 async fn dispatch_routes_sheets_add_sheet() {
1224 let guard = crate::drive::test_support::EnvGuard::take();
1225 guard.redirect_api_hosts_to_a_dead_port();
1226 let _dir = guard.clear_credentials();
1227
1228 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1229 command: sheets::SheetsSubcommands::AddSheet(sheets::structure::AddSheetCommand {
1230 target: sheets::structure::SpreadsheetIdArg {
1231 spreadsheet_id: "sheet-1".to_string(),
1232 },
1233 title: "Q3".to_string(),
1234 index: None,
1235 rows: None,
1236 columns: None,
1237 write: no_write_args(),
1238 }),
1239 });
1240 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1243 }
1244
1245 #[tokio::test]
1246 async fn dispatch_routes_sheets_rename_sheet() {
1247 let guard = crate::drive::test_support::EnvGuard::take();
1248 guard.redirect_api_hosts_to_a_dead_port();
1249 let _dir = guard.clear_credentials();
1250
1251 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1252 command: sheets::SheetsSubcommands::RenameSheet(
1253 sheets::structure::RenameSheetCommand {
1254 target: sheets::structure::SpreadsheetIdArg {
1255 spreadsheet_id: "sheet-1".to_string(),
1256 },
1257 sheet: "Q2".to_string(),
1258 title: "Q3".to_string(),
1259 write: no_write_args(),
1260 },
1261 ),
1262 });
1263 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1264 }
1265
1266 #[tokio::test]
1267 async fn dispatch_routes_sheets_insert_rows() {
1268 let guard = crate::drive::test_support::EnvGuard::take();
1269 guard.redirect_api_hosts_to_a_dead_port();
1270 let _dir = guard.clear_credentials();
1271
1272 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1273 command: sheets::SheetsSubcommands::InsertRows(sheets::structure::InsertRowsCommand {
1274 target: sheets::structure::SpreadsheetIdArg {
1275 spreadsheet_id: "sheet-1".to_string(),
1276 },
1277 sheet: "Q2".to_string(),
1278 at: 5,
1279 count: 3,
1280 write: no_write_args(),
1281 }),
1282 });
1283 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1284 }
1285
1286 #[tokio::test]
1287 async fn dispatch_routes_sheets_insert_columns() {
1288 let guard = crate::drive::test_support::EnvGuard::take();
1289 guard.redirect_api_hosts_to_a_dead_port();
1290 let _dir = guard.clear_credentials();
1291
1292 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1293 command: sheets::SheetsSubcommands::InsertColumns(
1294 sheets::structure::InsertColumnsCommand {
1295 target: sheets::structure::SpreadsheetIdArg {
1296 spreadsheet_id: "sheet-1".to_string(),
1297 },
1298 sheet: "Q2".to_string(),
1299 at: 2,
1300 count: 1,
1301 write: no_write_args(),
1302 },
1303 ),
1304 });
1305 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1306 }
1307
1308 #[tokio::test]
1309 async fn dispatch_routes_sheets_move_rows() {
1310 let guard = crate::drive::test_support::EnvGuard::take();
1311 guard.redirect_api_hosts_to_a_dead_port();
1312 let _dir = guard.clear_credentials();
1313
1314 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1315 command: sheets::SheetsSubcommands::MoveRows(sheets::structure::MoveRowsCommand {
1316 target: sheets::structure::SpreadsheetIdArg {
1317 spreadsheet_id: "sheet-1".to_string(),
1318 },
1319 sheet: "Q2".to_string(),
1320 at: 5,
1321 count: 3,
1322 before: 10,
1323 write: no_write_args(),
1324 }),
1325 });
1326 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1327 }
1328
1329 #[tokio::test]
1330 async fn dispatch_routes_sheets_move_columns() {
1331 let guard = crate::drive::test_support::EnvGuard::take();
1332 guard.redirect_api_hosts_to_a_dead_port();
1333 let _dir = guard.clear_credentials();
1334
1335 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1336 command: sheets::SheetsSubcommands::MoveColumns(
1337 sheets::structure::MoveColumnsCommand {
1338 target: sheets::structure::SpreadsheetIdArg {
1339 spreadsheet_id: "sheet-1".to_string(),
1340 },
1341 sheet: "Q2".to_string(),
1342 at: 5,
1343 count: 3,
1344 before: 10,
1345 write: no_write_args(),
1346 },
1347 ),
1348 });
1349 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1350 }
1351
1352 #[tokio::test]
1353 async fn dispatch_routes_sheets_move_chart() {
1354 let guard = crate::drive::test_support::EnvGuard::take();
1355 guard.redirect_api_hosts_to_a_dead_port();
1356 let _dir = guard.clear_credentials();
1357
1358 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1359 command: sheets::SheetsSubcommands::MoveChart(
1360 sheets::embedded_object::MoveChartCommand {
1361 spreadsheet_id: "sheet-1".to_string(),
1362 chart_id: 1,
1363 sheet: None,
1364 anchor: Some("F2".to_string()),
1365 offset_x: None,
1366 offset_y: None,
1367 width: None,
1368 height: None,
1369 new_sheet: false,
1370 write: helpers::StructureWriteArgs {
1371 dry_run: false,
1372 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1373 output: OutputFormat::Table,
1374 },
1375 },
1376 ),
1377 });
1378 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1379 }
1380
1381 #[tokio::test]
1382 async fn dispatch_routes_sheets_move_slicer() {
1383 let guard = crate::drive::test_support::EnvGuard::take();
1384 guard.redirect_api_hosts_to_a_dead_port();
1385 let _dir = guard.clear_credentials();
1386
1387 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1388 command: sheets::SheetsSubcommands::MoveSlicer(
1389 sheets::embedded_object::MoveSlicerCommand {
1390 spreadsheet_id: "sheet-1".to_string(),
1391 slicer_id: 1,
1392 sheet: None,
1393 anchor: Some("F2".to_string()),
1394 offset_x: None,
1395 offset_y: None,
1396 width: None,
1397 height: None,
1398 write: helpers::StructureWriteArgs {
1399 dry_run: false,
1400 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1401 output: OutputFormat::Table,
1402 },
1403 },
1404 ),
1405 });
1406 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1407 }
1408
1409 #[tokio::test]
1410 async fn dispatch_routes_sheets_update_chart_border() {
1411 let guard = crate::drive::test_support::EnvGuard::take();
1412 guard.redirect_api_hosts_to_a_dead_port();
1413 let _dir = guard.clear_credentials();
1414
1415 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1416 command: sheets::SheetsSubcommands::UpdateChartBorder(
1417 sheets::embedded_object::UpdateChartBorderCommand {
1418 spreadsheet_id: "sheet-1".to_string(),
1419 chart_id: 1,
1420 color: Some("#4A86E8".to_string()),
1421 clear: false,
1422 write: helpers::StructureWriteArgs {
1423 dry_run: false,
1424 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1425 output: OutputFormat::Table,
1426 },
1427 },
1428 ),
1429 });
1430 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1431 }
1432
1433 #[tokio::test]
1434 async fn dispatch_routes_sheets_delete_sheet() {
1435 let guard = crate::drive::test_support::EnvGuard::take();
1436 guard.redirect_api_hosts_to_a_dead_port();
1437 let _dir = guard.clear_credentials();
1438
1439 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1440 command: sheets::SheetsSubcommands::DeleteSheet(
1441 sheets::structure::DeleteSheetCommand {
1442 target: sheets::structure::SpreadsheetIdArg {
1443 spreadsheet_id: "sheet-1".to_string(),
1444 },
1445 sheet: "Q2".to_string(),
1446 write: no_write_args(),
1447 },
1448 ),
1449 });
1450 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1453 }
1454
1455 #[tokio::test]
1456 async fn dispatch_routes_sheets_delete_rows() {
1457 let guard = crate::drive::test_support::EnvGuard::take();
1458 guard.redirect_api_hosts_to_a_dead_port();
1459 let _dir = guard.clear_credentials();
1460
1461 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1462 command: sheets::SheetsSubcommands::DeleteRows(sheets::structure::DeleteRowsCommand {
1463 target: sheets::structure::SpreadsheetIdArg {
1464 spreadsheet_id: "sheet-1".to_string(),
1465 },
1466 sheet: "Q2".to_string(),
1467 at: 5,
1468 count: 3,
1469 write: no_write_args(),
1470 }),
1471 });
1472 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1473 }
1474
1475 #[tokio::test]
1476 async fn dispatch_routes_sheets_delete_columns() {
1477 let guard = crate::drive::test_support::EnvGuard::take();
1478 guard.redirect_api_hosts_to_a_dead_port();
1479 let _dir = guard.clear_credentials();
1480
1481 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1482 command: sheets::SheetsSubcommands::DeleteColumns(
1483 sheets::structure::DeleteColumnsCommand {
1484 target: sheets::structure::SpreadsheetIdArg {
1485 spreadsheet_id: "sheet-1".to_string(),
1486 },
1487 sheet: "Q2".to_string(),
1488 at: 2,
1489 count: 1,
1490 write: no_write_args(),
1491 },
1492 ),
1493 });
1494 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1495 }
1496
1497 #[tokio::test]
1498 async fn dispatch_routes_sheets_delete_range() {
1499 let guard = crate::drive::test_support::EnvGuard::take();
1500 guard.redirect_api_hosts_to_a_dead_port();
1501 let _dir = guard.clear_credentials();
1502
1503 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1504 command: sheets::SheetsSubcommands::DeleteRange(
1505 sheets::structure::DeleteRangeCommand {
1506 target: sheets::structure::SpreadsheetIdArg {
1507 spreadsheet_id: "sheet-1".to_string(),
1508 },
1509 sheet: "Q2".to_string(),
1510 range: sheets::structure::GridRangeArgs {
1511 start_row: 2,
1512 end_row: 4,
1513 start_column: 2,
1514 end_column: 3,
1515 shift: sheets::structure::ShiftArg::Rows,
1516 },
1517 write: no_write_args(),
1518 },
1519 ),
1520 });
1521 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1522 }
1523
1524 #[tokio::test]
1525 async fn dispatch_routes_sheets_duplicate_sheet() {
1526 let guard = crate::drive::test_support::EnvGuard::take();
1527 let _dir = guard.clear_credentials();
1528
1529 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1530 command: sheets::SheetsSubcommands::DuplicateSheet(
1531 sheets::structure::DuplicateSheetCommand {
1532 target: sheets::structure::SpreadsheetIdArg {
1533 spreadsheet_id: "sheet-1".to_string(),
1534 },
1535 sheet: "Q2".to_string(),
1536 title: Some("Q2 copy".to_string()),
1537 index: None,
1538 write: no_write_args(),
1539 },
1540 ),
1541 });
1542 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1543 }
1544
1545 #[tokio::test]
1546 async fn dispatch_routes_sheets_reorder_sheet() {
1547 let guard = crate::drive::test_support::EnvGuard::take();
1548 let _dir = guard.clear_credentials();
1549
1550 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1551 command: sheets::SheetsSubcommands::ReorderSheet(
1552 sheets::structure::ReorderSheetCommand {
1553 target: sheets::structure::SpreadsheetIdArg {
1554 spreadsheet_id: "sheet-1".to_string(),
1555 },
1556 sheet: "Q2".to_string(),
1557 index: 0,
1558 write: no_write_args(),
1559 },
1560 ),
1561 });
1562 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1563 }
1564
1565 #[tokio::test]
1566 async fn dispatch_routes_sheets_hide_sheet() {
1567 let guard = crate::drive::test_support::EnvGuard::take();
1568 let _dir = guard.clear_credentials();
1569
1570 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1571 command: sheets::SheetsSubcommands::HideSheet(sheets::structure::HideSheetCommand {
1572 target: sheets::structure::SpreadsheetIdArg {
1573 spreadsheet_id: "sheet-1".to_string(),
1574 },
1575 sheet: "Q2".to_string(),
1576 write: no_write_args(),
1577 }),
1578 });
1579 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1580 }
1581
1582 #[tokio::test]
1583 async fn dispatch_routes_sheets_show_sheet() {
1584 let guard = crate::drive::test_support::EnvGuard::take();
1585 let _dir = guard.clear_credentials();
1586
1587 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1588 command: sheets::SheetsSubcommands::ShowSheet(sheets::structure::ShowSheetCommand {
1589 target: sheets::structure::SpreadsheetIdArg {
1590 spreadsheet_id: "sheet-1".to_string(),
1591 },
1592 sheet: "Q2".to_string(),
1593 write: no_write_args(),
1594 }),
1595 });
1596 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1597 }
1598
1599 #[tokio::test]
1600 async fn dispatch_routes_sheets_update_workbook_properties() {
1601 let guard = crate::drive::test_support::EnvGuard::take();
1602 guard.redirect_api_hosts_to_a_dead_port();
1603 let _dir = guard.clear_credentials();
1604
1605 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1606 command: sheets::SheetsSubcommands::UpdateWorkbookProperties(
1607 sheets::structure::UpdateWorkbookPropertiesCommand {
1608 target: sheets::structure::SpreadsheetIdArg {
1609 spreadsheet_id: "sheet-1".to_string(),
1610 },
1611 locale: Some("en_US".to_string()),
1612 time_zone: None,
1613 auto_recalc: None,
1614 iterative_calculation: None,
1615 iterative_calculation_max_iterations: None,
1616 iterative_calculation_convergence_threshold: None,
1617 write: no_write_args(),
1618 },
1619 ),
1620 });
1621 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1624 }
1625
1626 #[tokio::test]
1627 async fn dispatch_routes_sheets_format_cells() {
1628 let guard = crate::drive::test_support::EnvGuard::take();
1629 let _dir = guard.clear_credentials();
1630
1631 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1632 command: sheets::SheetsSubcommands::FormatCells(sheets::format::FormatCellsCommand {
1633 spreadsheet_id: "sheet-1".to_string(),
1634 range: Some("A1:B2".to_string()),
1635 sheet: None,
1636 bold: Some(true),
1637 italic: None,
1638 strikethrough: None,
1639 underline: None,
1640 font_size: None,
1641 text_color: None,
1642 background: None,
1643 horizontal_align: Some(sheets::format::HorizontalAlign::Center),
1644 vertical_align: Some(sheets::format::VerticalAlign::Middle),
1645 number_format: Some("#,##0.00".to_string()),
1646 number_format_type: Some(sheets::format::NumberFormatType::Currency),
1647 wrap: Some(sheets::format::WrapStrategy::Wrap),
1648 font_family: None,
1649 text_rotation_angle: None,
1650 text_rotation_vertical: false,
1651 hyperlink_display_type: None,
1652 padding_top: None,
1653 padding_right: None,
1654 padding_bottom: None,
1655 padding_left: None,
1656 text_direction: None,
1657 write: helpers::StructureWriteArgs {
1658 dry_run: false,
1659 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1660 output: OutputFormat::Table,
1661 },
1662 }),
1663 });
1664 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1665 }
1666
1667 fn base_format_cells_command() -> sheets::format::FormatCellsCommand {
1668 sheets::format::FormatCellsCommand {
1669 spreadsheet_id: "sheet-1".to_string(),
1670 range: Some("A1:B2".to_string()),
1671 sheet: None,
1672 bold: None,
1673 italic: None,
1674 strikethrough: None,
1675 underline: None,
1676 font_size: None,
1677 text_color: None,
1678 background: None,
1679 horizontal_align: None,
1680 vertical_align: None,
1681 number_format: None,
1682 number_format_type: None,
1683 wrap: None,
1684 font_family: None,
1685 text_rotation_angle: None,
1686 text_rotation_vertical: false,
1687 hyperlink_display_type: None,
1688 padding_top: None,
1689 padding_right: None,
1690 padding_bottom: None,
1691 padding_left: None,
1692 text_direction: None,
1693 write: helpers::StructureWriteArgs {
1694 dry_run: false,
1695 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1696 output: OutputFormat::Table,
1697 },
1698 }
1699 }
1700
1701 #[tokio::test]
1702 async fn dispatch_routes_sheets_format_cells_text_rotation_angle() {
1703 let guard = crate::drive::test_support::EnvGuard::take();
1704 let _dir = guard.clear_credentials();
1705
1706 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1707 command: sheets::SheetsSubcommands::FormatCells(sheets::format::FormatCellsCommand {
1708 text_rotation_angle: Some(45),
1709 ..base_format_cells_command()
1710 }),
1711 });
1712 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1713 }
1714
1715 #[tokio::test]
1716 async fn dispatch_routes_sheets_format_cells_text_rotation_vertical() {
1717 let guard = crate::drive::test_support::EnvGuard::take();
1718 let _dir = guard.clear_credentials();
1719
1720 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1721 command: sheets::SheetsSubcommands::FormatCells(sheets::format::FormatCellsCommand {
1722 text_rotation_vertical: true,
1723 ..base_format_cells_command()
1724 }),
1725 });
1726 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1727 }
1728
1729 #[tokio::test]
1730 async fn dispatch_routes_sheets_update_borders() {
1731 let guard = crate::drive::test_support::EnvGuard::take();
1732 let _dir = guard.clear_credentials();
1733
1734 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1735 command: sheets::SheetsSubcommands::UpdateBorders(
1736 sheets::format::UpdateBordersCommand {
1737 spreadsheet_id: "sheet-1".to_string(),
1738 range: Some("A1:B2".to_string()),
1739 sheet: None,
1740 top: true,
1741 bottom: false,
1742 left: false,
1743 right: false,
1744 all: false,
1745 inner_horizontal: false,
1746 inner_vertical: false,
1747 style: sheets::format::BorderStyle::Dashed,
1748 color: Some("#FF0000".to_string()),
1749 write: helpers::StructureWriteArgs {
1750 dry_run: false,
1751 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1752 output: OutputFormat::Table,
1753 },
1754 },
1755 ),
1756 });
1757 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1758 }
1759
1760 #[tokio::test]
1761 async fn dispatch_routes_sheets_merge_cells() {
1762 let guard = crate::drive::test_support::EnvGuard::take();
1763 let _dir = guard.clear_credentials();
1764
1765 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1766 command: sheets::SheetsSubcommands::MergeCells(sheets::format::MergeCellsCommand {
1767 spreadsheet_id: "sheet-1".to_string(),
1768 range: Some("A1:B2".to_string()),
1769 sheet: None,
1770 r#type: sheets::format::MergeType::Rows,
1771 dry_run: false,
1772 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1773 output: OutputFormat::Table,
1774 }),
1775 });
1776 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1777 }
1778
1779 #[tokio::test]
1780 async fn dispatch_routes_sheets_unmerge_cells() {
1781 let guard = crate::drive::test_support::EnvGuard::take();
1782 let _dir = guard.clear_credentials();
1783
1784 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1785 command: sheets::SheetsSubcommands::UnmergeCells(sheets::format::UnmergeCellsCommand {
1786 spreadsheet_id: "sheet-1".to_string(),
1787 range: Some("A1:B2".to_string()),
1788 sheet: None,
1789 write: helpers::StructureWriteArgs {
1790 dry_run: false,
1791 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1792 output: OutputFormat::Table,
1793 },
1794 }),
1795 });
1796 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1797 }
1798
1799 #[tokio::test]
1800 async fn dispatch_routes_sheets_auto_resize_dimension() {
1801 let guard = crate::drive::test_support::EnvGuard::take();
1802 let _dir = guard.clear_credentials();
1803
1804 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1805 command: sheets::SheetsSubcommands::AutoResizeDimension(
1806 sheets::format::AutoResizeDimensionCommand {
1807 spreadsheet_id: "sheet-1".to_string(),
1808 sheet: "Q2".to_string(),
1809 dimension: sheets::format::DimensionArg::Columns,
1810 start: 1,
1811 end: 3,
1812 write: helpers::StructureWriteArgs {
1813 dry_run: false,
1814 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1815 output: OutputFormat::Table,
1816 },
1817 },
1818 ),
1819 });
1820 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1821 }
1822
1823 #[tokio::test]
1824 async fn dispatch_routes_sheets_update_dimension_properties() {
1825 let guard = crate::drive::test_support::EnvGuard::take();
1826 let _dir = guard.clear_credentials();
1827
1828 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1829 command: sheets::SheetsSubcommands::UpdateDimensionProperties(
1830 sheets::format::UpdateDimensionPropertiesCommand {
1831 spreadsheet_id: "sheet-1".to_string(),
1832 sheet: "Q2".to_string(),
1833 dimension: sheets::format::DimensionArg::Rows,
1834 start: 1,
1835 end: 3,
1836 pixel_size: 42,
1837 write: helpers::StructureWriteArgs {
1838 dry_run: false,
1839 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1840 output: OutputFormat::Table,
1841 },
1842 },
1843 ),
1844 });
1845 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1846 }
1847
1848 #[tokio::test]
1849 async fn dispatch_routes_sheets_set_data_validation() {
1850 let guard = crate::drive::test_support::EnvGuard::take();
1851 let _dir = guard.clear_credentials();
1852
1853 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1854 command: sheets::SheetsSubcommands::SetDataValidation(Box::new(
1855 sheets::validation::SetDataValidationCommand {
1856 spreadsheet_id: "sheet-1".to_string(),
1857 range: Some("A1:A10".to_string()),
1858 sheet: None,
1859 one_of_list: Some(vec!["yes".to_string(), "no".to_string()]),
1860 one_of_range: None,
1861 number_between: None,
1862 number_not_between: None,
1863 number_greater: None,
1864 number_greater_eq: None,
1865 number_less: None,
1866 number_less_eq: None,
1867 number_eq: None,
1868 number_not_eq: None,
1869 text_contains: None,
1870 text_not_contains: None,
1871 text_starts_with: None,
1872 text_ends_with: None,
1873 text_eq: None,
1874 date_after: None,
1875 date_before: None,
1876 date_on: None,
1877 date_between: None,
1878 blank: false,
1879 not_blank: false,
1880 checkbox: false,
1881 custom_formula: None,
1882 input_message: Some("Pick one".to_string()),
1883 show_warning: false,
1884 write: helpers::StructureWriteArgs {
1885 dry_run: false,
1886 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1887 output: OutputFormat::Table,
1888 },
1889 },
1890 )),
1891 });
1892 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1893 }
1894
1895 #[tokio::test]
1896 async fn dispatch_routes_sheets_clear_data_validation() {
1897 let guard = crate::drive::test_support::EnvGuard::take();
1898 let _dir = guard.clear_credentials();
1899
1900 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1901 command: sheets::SheetsSubcommands::ClearDataValidation(
1902 sheets::validation::ClearDataValidationCommand {
1903 spreadsheet_id: "sheet-1".to_string(),
1904 range: Some("A1:A10".to_string()),
1905 sheet: None,
1906 write: helpers::StructureWriteArgs {
1907 dry_run: false,
1908 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1909 output: OutputFormat::Table,
1910 },
1911 },
1912 ),
1913 });
1914 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1915 }
1916
1917 #[tokio::test]
1918 async fn dispatch_routes_sheets_set_developer_metadata() {
1919 let guard = crate::drive::test_support::EnvGuard::take();
1920 let _dir = guard.clear_credentials();
1921
1922 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1923 command: sheets::SheetsSubcommands::SetDeveloperMetadata(
1924 sheets::developer_metadata::SetDeveloperMetadataCommand {
1925 spreadsheet_id: "sheet-1".to_string(),
1926 key: "owner".to_string(),
1927 value: "team-a".to_string(),
1928 sheet: None,
1929 dimension: None,
1930 start: None,
1931 end: None,
1932 write: helpers::StructureWriteArgs {
1933 dry_run: false,
1934 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1935 output: OutputFormat::Table,
1936 },
1937 },
1938 ),
1939 });
1940 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1944 }
1945
1946 #[tokio::test]
1947 async fn dispatch_routes_sheets_delete_developer_metadata() {
1948 let guard = crate::drive::test_support::EnvGuard::take();
1949 let _dir = guard.clear_credentials();
1950
1951 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1952 command: sheets::SheetsSubcommands::DeleteDeveloperMetadata(
1953 sheets::developer_metadata::DeleteDeveloperMetadataCommand {
1954 spreadsheet_id: "sheet-1".to_string(),
1955 key: "owner".to_string(),
1956 sheet: None,
1957 dimension: None,
1958 start: None,
1959 end: None,
1960 dry_run: false,
1961 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
1962 output: OutputFormat::Table,
1963 },
1964 ),
1965 });
1966 assert!(cmd.dispatch(&dead_client()).await.is_ok());
1967 }
1968
1969 #[tokio::test]
1970 async fn dispatch_routes_sheets_search_developer_metadata() {
1971 let guard = crate::drive::test_support::EnvGuard::take();
1975 guard.redirect_api_hosts_to_a_dead_port();
1976
1977 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1978 command: sheets::SheetsSubcommands::SearchDeveloperMetadata(
1979 sheets::developer_metadata::SearchDeveloperMetadataCommand {
1980 spreadsheet_id: "sheet-1".to_string(),
1981 key: None,
1982 sheet: None,
1983 dimension: None,
1984 start: None,
1985 end: None,
1986 output: OutputFormat::Table,
1987 },
1988 ),
1989 });
1990 assert!(cmd.dispatch(&dead_client()).await.is_err());
1991 }
1992
1993 #[tokio::test]
1994 async fn dispatch_routes_sheets_protect_range() {
1995 let guard = crate::drive::test_support::EnvGuard::take();
1996 let _dir = guard.clear_credentials();
1997
1998 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
1999 command: sheets::SheetsSubcommands::ProtectRange(
2000 sheets::protection::ProtectRangeCommand {
2001 spreadsheet_id: "sheet-1".to_string(),
2002 range: Some("A1:B2".to_string()),
2003 sheet: None,
2004 whole_sheet: false,
2005 description: Some("locked".to_string()),
2006 warning_only: false,
2007 editors: vec!["alice@example.com".to_string()],
2008 write: helpers::StructureWriteArgs {
2009 dry_run: false,
2010 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2011 output: OutputFormat::Table,
2012 },
2013 },
2014 ),
2015 });
2016 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2017 }
2018
2019 #[tokio::test]
2020 async fn dispatch_routes_sheets_update_protection() {
2021 let guard = crate::drive::test_support::EnvGuard::take();
2022 let _dir = guard.clear_credentials();
2023
2024 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2025 command: sheets::SheetsSubcommands::UpdateProtection(
2026 sheets::protection::UpdateProtectionCommand {
2027 spreadsheet_id: "sheet-1".to_string(),
2028 range: Some("A1:B2".to_string()),
2029 sheet: None,
2030 whole_sheet: false,
2031 description: Some("still locked".to_string()),
2032 warning_only: Some(true),
2033 add_editors: vec!["bob@example.com".to_string()],
2034 remove_editors: vec!["alice@example.com".to_string()],
2035 write: helpers::StructureWriteArgs {
2036 dry_run: false,
2037 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2038 output: OutputFormat::Table,
2039 },
2040 },
2041 ),
2042 });
2043 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2044 }
2045
2046 #[tokio::test]
2047 async fn dispatch_routes_sheets_unprotect_range() {
2048 let guard = crate::drive::test_support::EnvGuard::take();
2049 let _dir = guard.clear_credentials();
2050
2051 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2052 command: sheets::SheetsSubcommands::UnprotectRange(
2053 sheets::protection::UnprotectRangeCommand {
2054 spreadsheet_id: "sheet-1".to_string(),
2055 range: Some("A1:B2".to_string()),
2056 sheet: None,
2057 whole_sheet: false,
2058 write: helpers::StructureWriteArgs {
2059 dry_run: false,
2060 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2061 output: OutputFormat::Table,
2062 },
2063 },
2064 ),
2065 });
2066 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2067 }
2068
2069 #[tokio::test]
2070 async fn dispatch_routes_sheets_list_protections() {
2071 let guard = crate::drive::test_support::EnvGuard::take();
2075 guard.redirect_api_hosts_to_a_dead_port();
2076
2077 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2078 command: sheets::SheetsSubcommands::ListProtections(
2079 sheets::protection::ListProtectionsCommand {
2080 spreadsheet_id: "sheet-1".to_string(),
2081 output: OutputFormat::Table,
2082 },
2083 ),
2084 });
2085 assert!(cmd.dispatch(&dead_client()).await.is_err());
2086 }
2087
2088 fn cell_empty_only_rule_args() -> sheets::conditional_format::ConditionalFormatRuleArgs {
2095 sheets::conditional_format::ConditionalFormatRuleArgs {
2096 number_between: None,
2097 number_not_between: None,
2098 number_greater: None,
2099 number_greater_eq: None,
2100 number_less: None,
2101 number_less_eq: None,
2102 number_eq: None,
2103 number_not_eq: None,
2104 text_contains: None,
2105 text_not_contains: None,
2106 text_starts_with: None,
2107 text_ends_with: None,
2108 text_eq: None,
2109 date_after: None,
2110 date_before: None,
2111 date_on: None,
2112 date_between: None,
2113 cell_empty: true,
2114 cell_not_empty: false,
2115 custom_formula: None,
2116 background: None,
2117 text_color: None,
2118 bold: None,
2119 gradient_min_color: None,
2120 gradient_max_color: None,
2121 gradient_mid_color: None,
2122 gradient_mid_type: None,
2123 gradient_mid_value: None,
2124 }
2125 }
2126
2127 #[tokio::test]
2128 async fn dispatch_routes_sheets_add_conditional_format() {
2129 let guard = crate::drive::test_support::EnvGuard::take();
2130 let _dir = guard.clear_credentials();
2131
2132 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2133 command: sheets::SheetsSubcommands::AddConditionalFormat(Box::new(
2134 sheets::conditional_format::AddConditionalFormatCommand {
2135 spreadsheet_id: "sheet-1".to_string(),
2136 sheet: "Q1".to_string(),
2137 ranges: vec!["A1:A10".to_string()],
2138 index: None,
2139 rule: cell_empty_only_rule_args(),
2140 write: helpers::StructureWriteArgs {
2141 dry_run: false,
2142 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2143 output: OutputFormat::Table,
2144 },
2145 },
2146 )),
2147 });
2148 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2149 }
2150
2151 #[tokio::test]
2152 async fn dispatch_routes_sheets_update_conditional_format() {
2153 let guard = crate::drive::test_support::EnvGuard::take();
2154 let _dir = guard.clear_credentials();
2155
2156 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2157 command: sheets::SheetsSubcommands::UpdateConditionalFormat(Box::new(
2158 sheets::conditional_format::UpdateConditionalFormatCommand {
2159 spreadsheet_id: "sheet-1".to_string(),
2160 sheet: "Q1".to_string(),
2161 index: 0,
2162 ranges: vec!["A1:A10".to_string()],
2163 rule: cell_empty_only_rule_args(),
2164 dry_run: false,
2165 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2166 output: OutputFormat::Table,
2167 },
2168 )),
2169 });
2170 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2171 }
2172
2173 #[tokio::test]
2174 async fn dispatch_routes_sheets_delete_conditional_format() {
2175 let guard = crate::drive::test_support::EnvGuard::take();
2176 let _dir = guard.clear_credentials();
2177
2178 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2179 command: sheets::SheetsSubcommands::DeleteConditionalFormat(
2180 sheets::conditional_format::DeleteConditionalFormatCommand {
2181 spreadsheet_id: "sheet-1".to_string(),
2182 sheet: "Q1".to_string(),
2183 index: 0,
2184 dry_run: false,
2185 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2186 output: OutputFormat::Table,
2187 },
2188 ),
2189 });
2190 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2191 }
2192
2193 #[tokio::test]
2194 async fn dispatch_routes_sheets_list_conditional_formats() {
2195 let guard = crate::drive::test_support::EnvGuard::take();
2199 guard.redirect_api_hosts_to_a_dead_port();
2200
2201 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2202 command: sheets::SheetsSubcommands::ListConditionalFormats(
2203 sheets::conditional_format::ListConditionalFormatsCommand {
2204 spreadsheet_id: "sheet-1".to_string(),
2205 output: OutputFormat::Table,
2206 },
2207 ),
2208 });
2209 assert!(cmd.dispatch(&dead_client()).await.is_err());
2210 }
2211
2212 #[tokio::test]
2213 async fn dispatch_routes_sheets_set_basic_filter() {
2214 let guard = crate::drive::test_support::EnvGuard::take();
2215 let _dir = guard.clear_credentials();
2216
2217 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2218 command: sheets::SheetsSubcommands::SetBasicFilter(
2219 sheets::filter::SetBasicFilterCommand {
2220 spreadsheet_id: "sheet-1".to_string(),
2221 sheet: "Q1".to_string(),
2222 range: "A1:D10".to_string(),
2223 sort_by: vec!["0:asc".to_string()],
2224 hide_values: vec!["1:Closed".to_string()],
2225 write: helpers::StructureWriteArgs {
2226 dry_run: false,
2227 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2228 output: OutputFormat::Table,
2229 },
2230 },
2231 ),
2232 });
2233 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2234 }
2235
2236 #[tokio::test]
2237 async fn dispatch_routes_sheets_clear_basic_filter() {
2238 let guard = crate::drive::test_support::EnvGuard::take();
2239 let _dir = guard.clear_credentials();
2240
2241 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2242 command: sheets::SheetsSubcommands::ClearBasicFilter(
2243 sheets::filter::ClearBasicFilterCommand {
2244 spreadsheet_id: "sheet-1".to_string(),
2245 sheet: "Q1".to_string(),
2246 write: helpers::StructureWriteArgs {
2247 dry_run: false,
2248 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2249 output: OutputFormat::Table,
2250 },
2251 },
2252 ),
2253 });
2254 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2255 }
2256
2257 #[tokio::test]
2258 async fn dispatch_routes_sheets_add_filter_view() {
2259 let guard = crate::drive::test_support::EnvGuard::take();
2260 let _dir = guard.clear_credentials();
2261
2262 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2263 command: sheets::SheetsSubcommands::AddFilterView(
2264 sheets::filter::AddFilterViewCommand {
2265 spreadsheet_id: "sheet-1".to_string(),
2266 sheet: "Q1".to_string(),
2267 range: "A1:D10".to_string(),
2268 title: Some("Open only".to_string()),
2269 sort_by: Vec::new(),
2270 hide_values: Vec::new(),
2271 write: helpers::StructureWriteArgs {
2272 dry_run: false,
2273 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2274 output: OutputFormat::Table,
2275 },
2276 },
2277 ),
2278 });
2279 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2280 }
2281
2282 #[tokio::test]
2283 async fn dispatch_routes_sheets_update_filter_view() {
2284 let guard = crate::drive::test_support::EnvGuard::take();
2285 let _dir = guard.clear_credentials();
2286
2287 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2288 command: sheets::SheetsSubcommands::UpdateFilterView(
2289 sheets::filter::UpdateFilterViewCommand {
2290 spreadsheet_id: "sheet-1".to_string(),
2291 filter_view_id: 7,
2292 sheet: None,
2293 range: None,
2294 title: Some("Renamed".to_string()),
2295 sort_by: Vec::new(),
2296 hide_values: Vec::new(),
2297 clear_sort: false,
2298 clear_criteria: false,
2299 write: helpers::StructureWriteArgs {
2300 dry_run: false,
2301 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2302 output: OutputFormat::Table,
2303 },
2304 },
2305 ),
2306 });
2307 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2308 }
2309
2310 #[tokio::test]
2311 async fn dispatch_routes_sheets_delete_filter_view() {
2312 let guard = crate::drive::test_support::EnvGuard::take();
2313 let _dir = guard.clear_credentials();
2314
2315 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2316 command: sheets::SheetsSubcommands::DeleteFilterView(
2317 sheets::filter::DeleteFilterViewCommand {
2318 spreadsheet_id: "sheet-1".to_string(),
2319 filter_view_id: 7,
2320 write: helpers::StructureWriteArgs {
2321 dry_run: false,
2322 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2323 output: OutputFormat::Table,
2324 },
2325 },
2326 ),
2327 });
2328 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2329 }
2330
2331 #[tokio::test]
2332 async fn dispatch_routes_sheets_list_filter_views() {
2333 let guard = crate::drive::test_support::EnvGuard::take();
2337 guard.redirect_api_hosts_to_a_dead_port();
2338
2339 let cmd = DriveSubcommands::Sheets(sheets::SheetsCommand {
2340 command: sheets::SheetsSubcommands::ListFilterViews(
2341 sheets::filter::ListFilterViewsCommand {
2342 spreadsheet_id: "sheet-1".to_string(),
2343 output: OutputFormat::Table,
2344 },
2345 ),
2346 });
2347 assert!(cmd.dispatch(&dead_client()).await.is_err());
2348 }
2349
2350 #[tokio::test]
2351 async fn dispatch_routes_search() {
2352 let cmd = DriveSubcommands::Search(search::SearchCommand {
2353 query: "name contains 'x'".to_string(),
2354 limit: 10,
2355 output: OutputFormat::Table,
2356 });
2357 assert!(cmd.dispatch(&dead_client()).await.is_err());
2358 }
2359
2360 #[tokio::test]
2361 async fn dispatch_routes_read() {
2362 let cmd = DriveSubcommands::Read(read::ReadCommand {
2363 file_id: "f1".to_string(),
2364 content: false,
2365 export_mime_type: None,
2366 out_file: None,
2367 verify: false,
2368 output: OutputFormat::Table,
2369 });
2370 assert!(cmd.dispatch(&dead_client()).await.is_err());
2371 }
2372
2373 #[tokio::test]
2374 async fn dispatch_routes_sync() {
2375 let dir = tempfile::tempdir().unwrap();
2376 let cmd = DriveSubcommands::Sync(sync::SyncCommand {
2377 folder_id: "root".into(),
2378 dest: dir.path().into(),
2379 export_mime_type: None,
2380 verify: false,
2381 dry_run: true,
2382 output: OutputFormat::Json,
2383 });
2384 assert!(cmd.dispatch(&dead_client()).await.is_err());
2385 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0);
2386 }
2387
2388 #[test]
2389 fn sync_parser_requires_destination_and_accepts_options() {
2390 use clap::Parser;
2391 assert!(DriveCommand::try_parse_from(["drive", "sync", "root"]).is_err());
2392 let cmd = DriveCommand::try_parse_from([
2393 "drive",
2394 "sync",
2395 "root",
2396 "--dest",
2397 "mirror",
2398 "--verify",
2399 "--dry-run",
2400 "--export-mime-type",
2401 "application/pdf",
2402 "-o",
2403 "json",
2404 ])
2405 .unwrap();
2406 let DriveSubcommands::Sync(sync) = cmd.command else {
2407 panic!("expected sync"); };
2409 assert!(sync.verify && sync.dry_run);
2410 assert_eq!(sync.export_mime_type.as_deref(), Some("application/pdf"));
2411 }
2412
2413 #[tokio::test]
2414 async fn dispatch_routes_dedupe() {
2415 let cmd = DriveSubcommands::Dedupe(dedupe::DedupeCommand {
2416 query: "name contains 'x'".to_string(),
2417 limit: 10,
2418 output: OutputFormat::Table,
2419 });
2420 assert!(cmd.dispatch(&dead_client()).await.is_err());
2421 }
2422
2423 #[tokio::test]
2424 async fn dispatch_routes_create() {
2425 let guard = crate::drive::test_support::EnvGuard::take();
2436 let _dir = guard.clear_credentials();
2437
2438 let cmd = DriveSubcommands::Create(create::CreateCommand {
2439 name: "New File".to_string(),
2440 parent: "parent-1".to_string(),
2441 folder: false,
2442 mime_type: None,
2443 dry_run: false,
2444 output: OutputFormat::Table,
2445 });
2446 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2447 }
2448
2449 #[tokio::test]
2450 async fn dispatch_routes_upload() {
2451 let guard = crate::drive::test_support::EnvGuard::take();
2454 let _dir = guard.clear_credentials();
2455 let content_dir = tempfile::tempdir().unwrap();
2456 let local_path = content_dir.path().join("upload-me.txt");
2457 std::fs::write(&local_path, b"content").unwrap();
2458
2459 let cmd = DriveSubcommands::Upload(upload::UploadCommand {
2460 local_path,
2461 parent: "parent-1".to_string(),
2462 name: None,
2463 mime_type: None,
2464 dry_run: false,
2465 output: OutputFormat::Table,
2466 });
2467 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2468 }
2469
2470 #[tokio::test]
2471 async fn dispatch_routes_edit() {
2472 let guard = crate::drive::test_support::EnvGuard::take();
2475 let _dir = guard.clear_credentials();
2476 let content_dir = tempfile::tempdir().unwrap();
2477 let content_path = content_dir.path().join("new-content.txt");
2478 std::fs::write(&content_path, b"content").unwrap();
2479
2480 let cmd = DriveSubcommands::Edit(edit::EditCommand {
2481 file_id: "f1".to_string(),
2482 content: content_path.to_str().unwrap().to_string(),
2483 mime_type: None,
2484 dry_run: false,
2485 lease: crate::cli::drive::helpers::LeaseTokenArg { lease: None },
2486 output: OutputFormat::Table,
2487 });
2488 assert!(cmd.dispatch(&dead_client()).await.is_ok());
2489 }
2490
2491 #[tokio::test]
2492 async fn dispatch_routes_rename() {
2493 let cmd = DriveSubcommands::Rename(rename::RenameCommand {
2494 file_id: "f1".to_string(),
2495 new_name: "New Name".to_string(),
2496 dry_run: false,
2497 output: OutputFormat::Table,
2498 });
2499 assert!(cmd.dispatch(&dead_client()).await.is_err());
2500 }
2501
2502 #[tokio::test]
2503 async fn dispatch_routes_move() {
2504 let cmd = DriveSubcommands::Move(move_file::MoveCommand {
2505 file_ids: vec!["f1".to_string()],
2506 to: "dest1".to_string(),
2507 allow_visibility_increase: false,
2508 allow_visibility_decrease: false,
2509 allow_drive_boundary_crossing: false,
2510 dry_run: false,
2511 output: OutputFormat::Table,
2512 });
2513 assert!(cmd.dispatch(&dead_client()).await.is_err());
2514 }
2515}