use anyhow::Result;
use clap::Parser;
use crate::cli::drive::format::{output_as, sanitize_for_terminal, OutputFormat};
use crate::cli::drive::helpers;
use crate::drive::client::DriveClient;
use crate::drive::sheets::api::SheetsApi;
use crate::drive::sheets::client::SheetsClient;
use crate::drive::sheets::embedded_object::{
charts_from_workbook, describe_lines, embedded_object, slicers_from_workbook,
EmbeddedObjectOptions, EmbeddedObjectSummary, EmbeddedObjectVerb,
};
#[derive(Parser)]
pub struct AddChartCommand {
pub spreadsheet_id: String,
#[arg(long = "type", value_name = "TYPE")]
pub chart_type: String,
#[arg(long, value_name = "A1")]
pub domain: String,
#[arg(long, value_name = "A1")]
pub series: Vec<String>,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub subtitle: Option<String>,
#[arg(long, value_name = "POSITION")]
pub legend: Option<String>,
#[arg(long, value_name = "MODE")]
pub stacked: Option<String>,
#[arg(long, value_name = "N")]
pub header_count: Option<i64>,
#[arg(long)]
pub horizontal_axis_title: Option<String>,
#[arg(long)]
pub vertical_axis_title: Option<String>,
#[arg(long, value_name = "0.0-1.0", allow_hyphen_values = true)]
pub pie_hole: Option<f64>,
#[arg(long, value_name = "A1", conflicts_with = "new_sheet")]
pub anchor: Option<String>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub offset_x: Option<i64>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub offset_y: Option<i64>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub width: Option<i64>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub height: Option<i64>,
#[arg(long)]
pub new_sheet: bool,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl AddChartCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::AddChart {
chart_type: self.chart_type,
domain: self.domain,
series: self.series,
sheet: self.sheet,
title: self.title,
subtitle: self.subtitle,
legend: self.legend,
stacked: self.stacked,
header_count: self.header_count,
horizontal_axis_title: self.horizontal_axis_title,
vertical_axis_title: self.vertical_axis_title,
pie_hole: self.pie_hole,
anchor: self.anchor,
offset_x: self.offset_x,
offset_y: self.offset_y,
width: self.width,
height: self.height,
new_sheet: self.new_sheet,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_embedded_object(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct UpdateChartCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "ID")]
pub chart_id: i64,
#[arg(long = "type", value_name = "TYPE")]
pub chart_type: Option<String>,
#[arg(long, value_name = "A1")]
pub domain: Option<String>,
#[arg(long, value_name = "A1")]
pub series: Vec<String>,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub subtitle: Option<String>,
#[arg(long, value_name = "POSITION")]
pub legend: Option<String>,
#[arg(long, value_name = "MODE")]
pub stacked: Option<String>,
#[arg(long, value_name = "N")]
pub header_count: Option<i64>,
#[arg(long)]
pub horizontal_axis_title: Option<String>,
#[arg(long)]
pub vertical_axis_title: Option<String>,
#[arg(long, value_name = "0.0-1.0", allow_hyphen_values = true)]
pub pie_hole: Option<f64>,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl UpdateChartCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::UpdateChart {
chart_id: self.chart_id,
chart_type: self.chart_type,
domain: self.domain,
series: self.series,
sheet: self.sheet,
title: self.title,
subtitle: self.subtitle,
legend: self.legend,
stacked: self.stacked,
header_count: self.header_count,
horizontal_axis_title: self.horizontal_axis_title,
vertical_axis_title: self.vertical_axis_title,
pie_hole: self.pie_hole,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_embedded_object(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct DeleteChartCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "ID")]
pub chart_id: i64,
#[arg(long)]
pub dry_run: bool,
#[command(flatten)]
pub lease: crate::cli::drive::helpers::LeaseTokenArg,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl DeleteChartCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::DeleteChart {
chart_id: self.chart_id,
},
dry_run: self.dry_run,
lease_token: self.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.dry_run)?,
};
run_embedded_object(client, &opts, &self.output).await
}
}
#[derive(Parser)]
pub struct ListChartsCommand {
pub spreadsheet_id: String,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl ListChartsCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let sheets = SheetsClient::from_drive_client(client)?;
let api = SheetsApi::new(&sheets);
let workbook = api
.get_spreadsheet_with_embedded_objects(&self.spreadsheet_id)
.await?;
if output_as(&workbook, &self.output)? {
return Ok(());
}
let rows: Vec<String> = charts_from_workbook(&workbook)
.iter()
.map(describe_summary_line)
.collect();
helpers::print_list(&rows, "No charts.");
Ok(())
}
}
#[derive(Parser)]
pub struct AddSlicerCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long, value_name = "A1")]
pub range: String,
#[arg(long, value_name = "N")]
pub column: i64,
#[arg(long = "hide-values", value_name = "VALUES", value_delimiter = ',')]
pub hide_values: Vec<String>,
#[arg(long)]
pub title: Option<String>,
#[arg(long, value_name = "BOOL")]
pub apply_to_pivot_tables: Option<bool>,
#[arg(long, value_name = "A1")]
pub anchor: String,
#[arg(long, value_name = "PX")]
pub offset_x: Option<i64>,
#[arg(long, value_name = "PX")]
pub offset_y: Option<i64>,
#[arg(long, value_name = "PX")]
pub width: Option<i64>,
#[arg(long, value_name = "PX")]
pub height: Option<i64>,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl AddSlicerCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::AddSlicer {
sheet: self.sheet,
range: self.range,
column: self.column,
hide_values: self.hide_values,
title: self.title,
apply_to_pivot_tables: self.apply_to_pivot_tables,
anchor: self.anchor,
offset_x: self.offset_x,
offset_y: self.offset_y,
width: self.width,
height: self.height,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_embedded_object(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct UpdateSlicerCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "ID")]
pub slicer_id: i64,
#[arg(long, value_name = "NAME")]
pub sheet: Option<String>,
#[arg(long, value_name = "A1")]
pub range: Option<String>,
#[arg(long, value_name = "N")]
pub column: Option<i64>,
#[arg(
long = "hide-values",
value_name = "VALUES",
value_delimiter = ',',
conflicts_with = "clear_criteria"
)]
pub hide_values: Vec<String>,
#[arg(long, conflicts_with = "hide_values")]
pub clear_criteria: bool,
#[arg(long)]
pub title: Option<String>,
#[arg(long, value_name = "BOOL")]
pub apply_to_pivot_tables: Option<bool>,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl UpdateSlicerCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::UpdateSlicer {
slicer_id: self.slicer_id,
sheet: self.sheet,
range: self.range,
column: self.column,
hide_values: self.hide_values,
clear_criteria: self.clear_criteria,
title: self.title,
apply_to_pivot_tables: self.apply_to_pivot_tables,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_embedded_object(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct DeleteSlicerCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "ID")]
pub slicer_id: i64,
#[arg(long)]
pub dry_run: bool,
#[command(flatten)]
pub lease: crate::cli::drive::helpers::LeaseTokenArg,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl DeleteSlicerCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::DeleteSlicer {
slicer_id: self.slicer_id,
},
dry_run: self.dry_run,
lease_token: self.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.dry_run)?,
};
run_embedded_object(client, &opts, &self.output).await
}
}
#[derive(Parser)]
pub struct ListSlicersCommand {
pub spreadsheet_id: String,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl ListSlicersCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let sheets = SheetsClient::from_drive_client(client)?;
let api = SheetsApi::new(&sheets);
let workbook = api
.get_spreadsheet_with_embedded_objects(&self.spreadsheet_id)
.await?;
if output_as(&workbook, &self.output)? {
return Ok(());
}
let rows: Vec<String> = slicers_from_workbook(&workbook)
.iter()
.map(describe_summary_line)
.collect();
helpers::print_list(&rows, "No slicers.");
Ok(())
}
}
#[derive(Parser)]
pub struct MoveChartCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "ID")]
pub chart_id: i64,
#[arg(long, value_name = "NAME", requires = "anchor")]
pub sheet: Option<String>,
#[arg(long, value_name = "A1", conflicts_with = "new_sheet")]
pub anchor: Option<String>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub offset_x: Option<i64>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub offset_y: Option<i64>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub width: Option<i64>,
#[arg(long, value_name = "PX", conflicts_with = "new_sheet")]
pub height: Option<i64>,
#[arg(long)]
pub new_sheet: bool,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl MoveChartCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::MoveChart {
chart_id: self.chart_id,
sheet: self.sheet,
anchor: self.anchor,
offset_x: self.offset_x,
offset_y: self.offset_y,
width: self.width,
height: self.height,
new_sheet: self.new_sheet,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_embedded_object(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct MoveSlicerCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "ID")]
pub slicer_id: i64,
#[arg(long, value_name = "NAME", requires = "anchor")]
pub sheet: Option<String>,
#[arg(long, value_name = "A1")]
pub anchor: Option<String>,
#[arg(long, value_name = "PX")]
pub offset_x: Option<i64>,
#[arg(long, value_name = "PX")]
pub offset_y: Option<i64>,
#[arg(long, value_name = "PX")]
pub width: Option<i64>,
#[arg(long, value_name = "PX")]
pub height: Option<i64>,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl MoveSlicerCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::MoveSlicer {
slicer_id: self.slicer_id,
sheet: self.sheet,
anchor: self.anchor,
offset_x: self.offset_x,
offset_y: self.offset_y,
width: self.width,
height: self.height,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_embedded_object(client, &opts, &self.write.output).await
}
}
#[derive(Parser)]
pub struct UpdateChartBorderCommand {
pub spreadsheet_id: String,
#[arg(long, value_name = "ID")]
pub chart_id: i64,
#[arg(long, value_name = "#RRGGBB", conflicts_with = "clear")]
pub color: Option<String>,
#[arg(long, conflicts_with = "color")]
pub clear: bool,
#[command(flatten)]
pub write: helpers::StructureWriteArgs,
}
impl UpdateChartBorderCommand {
pub async fn execute(self, client: &DriveClient) -> Result<()> {
let opts = EmbeddedObjectOptions {
spreadsheet_id: self.spreadsheet_id,
verb: EmbeddedObjectVerb::UpdateChartBorder {
chart_id: self.chart_id,
color: self.color,
clear: self.clear,
},
dry_run: self.write.dry_run,
lease_token: self.write.lease.lease,
ledger_path: helpers::resolve_ledger_path(self.write.dry_run)?,
};
run_embedded_object(client, &opts, &self.write.output).await
}
}
fn describe_summary_line(object: &EmbeddedObjectSummary) -> String {
let kind = object
.chart_type
.as_deref()
.map_or_else(|| object.kind.clone(), |t| format!("{} ({t})", object.kind));
let title = object
.title
.as_deref()
.map_or_else(String::new, |t| format!(" '{t}'"));
format!(
"id {}: {kind}{title}, anchored {}",
object.object_id, object.position
)
}
async fn run_embedded_object(
client: &DriveClient,
opts: &EmbeddedObjectOptions,
output: &OutputFormat,
) -> Result<()> {
let sheets = SheetsClient::from_drive_client(client)?;
let rules = helpers::active_account_rules()?;
let outcome = embedded_object(client, &sheets, opts, &rules).await;
if output_as(&outcome, output)? {
return Ok(());
}
let lines: Vec<String> = describe_lines(&outcome)
.into_iter()
.map(|line| sanitize_for_terminal(&line))
.collect();
println!("{}", lines.join("\n"));
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::drive::auth::{DriveCredentials, DriveGrantedScopes};
use crate::drive::sheets::client::SHEETS_API_URL;
use crate::utils::secret::Secret;
fn test_credentials() -> DriveCredentials {
DriveCredentials {
client_id: "client-1".to_string(),
client_secret: Secret::new("secret-1"),
refresh_token: Secret::new("refresh-1"),
scope: DriveGrantedScopes::READONLY,
}
}
async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> DriveClient {
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::path("/token"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "test-token",
"expires_in": 3600,
})),
)
.mount(server)
.await;
let mut client = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
crate::drive::client::test_support::replace_session(
&mut client,
&test_credentials(),
&format!("{}/token", server.uri()),
);
client
}
async fn mount_orphan_sheet(server: &wiremock::MockServer) {
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/drive/v3/files/sheet-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "sheet-1", "name": "sheet-1",
"mimeType": crate::drive::types::GOOGLE_SHEET_MIME_TYPE,
"parents": [],
})),
)
.mount(server)
.await;
}
fn lease_arg() -> crate::cli::drive::helpers::LeaseTokenArg {
crate::cli::drive::helpers::LeaseTokenArg { lease: None }
}
#[tokio::test]
async fn add_chart_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
let cmd = AddChartCommand {
spreadsheet_id: "sheet-1".to_string(),
chart_type: "column".to_string(),
domain: "A1:A10".to_string(),
series: vec!["B1:B10".to_string()],
sheet: Some("Sheet1".to_string()),
title: None,
subtitle: None,
legend: None,
stacked: None,
header_count: None,
horizontal_axis_title: None,
vertical_axis_title: None,
pie_hole: None,
anchor: Some("E2".to_string()),
offset_x: None,
offset_y: None,
width: None,
height: None,
new_sheet: false,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: lease_arg(),
output: OutputFormat::Table,
},
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn update_chart_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
let cmd = UpdateChartCommand {
spreadsheet_id: "sheet-1".to_string(),
chart_id: 1,
chart_type: None,
domain: None,
series: Vec::new(),
sheet: None,
title: Some("New title".to_string()),
subtitle: None,
legend: None,
stacked: None,
header_count: None,
horizontal_axis_title: None,
vertical_axis_title: None,
pie_hole: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: lease_arg(),
output: OutputFormat::Table,
},
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn delete_chart_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
let cmd = DeleteChartCommand {
spreadsheet_id: "sheet-1".to_string(),
chart_id: 1,
dry_run: false,
lease: lease_arg(),
output: OutputFormat::Table,
};
assert!(cmd.execute(&client).await.is_ok());
let cmd = DeleteChartCommand {
spreadsheet_id: "sheet-1".to_string(),
chart_id: 1,
dry_run: false,
lease: lease_arg(),
output: OutputFormat::Json,
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn list_charts_prints_every_chart() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/v4/spreadsheets/sheet-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"spreadsheetId": "sheet-1",
"properties": {"title": "Budget"},
"sheets": [{
"properties": {"sheetId": 0, "title": "Sheet1"},
"charts": [{
"chartId": 1,
"spec": {
"title": "Sales",
"basicChart": {"chartType": "COLUMN"},
},
"position": {
"overlayPosition": {
"anchorCell": {"sheetId": 0, "rowIndex": 1, "columnIndex": 4},
},
},
}],
}],
})),
)
.mount(&server)
.await;
let cmd = ListChartsCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Table,
};
assert!(cmd.execute(&client).await.is_ok());
let cmd = ListChartsCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Json,
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn list_charts_reads_back_a_chart_anchored_at_a1_on_sheet_0() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/v4/spreadsheets/sheet-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"spreadsheetId": "sheet-1",
"properties": {"title": "Budget"},
"sheets": [{
"properties": {"sheetId": 0, "title": "Sheet1"},
"charts": [{
"chartId": 1,
"spec": {
"title": "Sales",
"basicChart": {"chartType": "COLUMN"},
},
"position": {
"overlayPosition": {"anchorCell": {}},
},
}],
}],
})),
)
.mount(&server)
.await;
let cmd = ListChartsCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Table,
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn add_slicer_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
let cmd = AddSlicerCommand {
spreadsheet_id: "sheet-1".to_string(),
sheet: Some("Sheet1".to_string()),
range: "A1:D10".to_string(),
column: 1,
hide_values: vec!["Closed".to_string()],
title: None,
apply_to_pivot_tables: None,
anchor: "F2".to_string(),
offset_x: None,
offset_y: None,
width: None,
height: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: lease_arg(),
output: OutputFormat::Table,
},
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn update_slicer_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
let cmd = UpdateSlicerCommand {
spreadsheet_id: "sheet-1".to_string(),
slicer_id: 1,
sheet: None,
range: None,
column: None,
hide_values: Vec::new(),
clear_criteria: true,
title: None,
apply_to_pivot_tables: None,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: lease_arg(),
output: OutputFormat::Table,
},
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn delete_slicer_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
let cmd = DeleteSlicerCommand {
spreadsheet_id: "sheet-1".to_string(),
slicer_id: 1,
dry_run: false,
lease: lease_arg(),
output: OutputFormat::Table,
};
assert!(cmd.execute(&client).await.is_ok());
}
#[tokio::test]
async fn move_chart_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
for output in [OutputFormat::Table, OutputFormat::Json] {
let cmd = 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: lease_arg(),
output,
},
};
assert!(cmd.execute(&client).await.is_ok());
}
}
#[tokio::test]
async fn move_slicer_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
for output in [OutputFormat::Table, OutputFormat::Json] {
let cmd = 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: lease_arg(),
output,
},
};
assert!(cmd.execute(&client).await.is_ok());
}
}
#[tokio::test]
async fn update_chart_border_command_reports_blocked_with_no_rules() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
mount_orphan_sheet(&server).await;
for output in [OutputFormat::Table, OutputFormat::Json] {
let cmd = UpdateChartBorderCommand {
spreadsheet_id: "sheet-1".to_string(),
chart_id: 1,
color: Some("#4A86E8".to_string()),
clear: false,
write: helpers::StructureWriteArgs {
dry_run: false,
lease: lease_arg(),
output,
},
};
assert!(cmd.execute(&client).await.is_ok());
}
}
#[tokio::test]
async fn list_slicers_prints_every_slicer() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let server = wiremock::MockServer::start().await;
let client = client_with_bootstrapped_token(&server).await;
std::env::set_var(SHEETS_API_URL, server.uri());
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/v4/spreadsheets/sheet-1"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"spreadsheetId": "sheet-1",
"properties": {"title": "Budget"},
"sheets": [{
"properties": {"sheetId": 0, "title": "Sheet1"},
"slicers": [{
"slicerId": 4,
"spec": {"title": "Region"},
"position": {
"overlayPosition": {
"anchorCell": {"sheetId": 0, "rowIndex": 0, "columnIndex": 5},
},
},
}],
}],
})),
)
.mount(&server)
.await;
let cmd = ListSlicersCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Table,
};
assert!(cmd.execute(&client).await.is_ok());
let cmd = ListSlicersCommand {
spreadsheet_id: "sheet-1".to_string(),
output: OutputFormat::Json,
};
assert!(cmd.execute(&client).await.is_ok());
}
}