Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tasks-due-time-warning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Warn when a Google Tasks request includes a non-midnight `due` time, since the API stores only the date and ignores time-of-day.
105 changes: 105 additions & 0 deletions crates/google-workspace-cli/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,43 @@ fn parse_and_validate_inputs(
})
}

fn extract_due_string_with_non_midnight_time(body: &Value) -> Option<String> {
let due = body.get("due")?.as_str()?;
let (_, time_part) = due.split_once('T')?;
let time_only = match time_part.find(|c: char| c == 'Z' || c == '+' || c == '-') {
Some(idx) => &time_part[..idx],
None => time_part,
};

if time_only
.chars()
.any(|c| c.is_ascii_digit() && c != '0')
{
Some(due.to_string())
} else {
None
}
}
Comment thread
jeevan6996 marked this conversation as resolved.

fn tasks_due_time_truncated_warning(
doc: &RestDescription,
method: &RestMethod,
body: Option<&Value>,
) -> Option<String> {
if doc.name != "tasks" {
return None;
}
if !matches!(method.http_method.as_str(), "POST" | "PUT" | "PATCH") {
return None;
}
let body = body?;
let due = extract_due_string_with_non_midnight_time(body)?;
Some(format!(
"Google Tasks API stores only the due date; time-of-day is ignored (received due='{}').",
due
))
}

/// Build an HTTP request with auth, query params, page token, and body/multipart attachment.
#[allow(clippy::too_many_arguments)]
async fn build_http_request(
Expand Down Expand Up @@ -413,6 +450,10 @@ pub async fn execute_method(
) -> Result<Option<Value>, GwsError> {
let input = parse_and_validate_inputs(doc, method, params_json, body_json, upload.is_some())?;

if let Some(msg) = tasks_due_time_truncated_warning(doc, method, input.body.as_ref()) {
eprintln!("Warning: {}", sanitize_for_terminal(&msg));
}

if dry_run {
let dry_run_info = json!({
"dry_run": true,
Expand Down Expand Up @@ -1209,6 +1250,70 @@ mod tests {
assert_ne!(AuthMethod::OAuth, AuthMethod::None);
}

#[test]
fn tasks_due_time_truncated_warning_detects_non_midnight_due_time() {
let doc = RestDescription {
name: "tasks".to_string(),
..Default::default()
};
let method = RestMethod {
http_method: "POST".to_string(),
..Default::default()
};
let body = json!({"due": "2026-04-12T15:30:00.000Z"});

let warning = tasks_due_time_truncated_warning(&doc, &method, Some(&body));
assert!(warning.is_some());
}

#[test]
fn tasks_due_time_truncated_warning_ignores_midnight_due_time() {
let doc = RestDescription {
name: "tasks".to_string(),
..Default::default()
};
let method = RestMethod {
http_method: "PATCH".to_string(),
..Default::default()
};
let body = json!({"due": "2026-04-12T00:00:00.000Z"});

let warning = tasks_due_time_truncated_warning(&doc, &method, Some(&body));
assert!(warning.is_none());
}

#[test]
fn tasks_due_time_truncated_warning_detects_fractional_seconds() {
let doc = RestDescription {
name: "tasks".to_string(),
..Default::default()
};
let method = RestMethod {
http_method: "PATCH".to_string(),
..Default::default()
};
let body = json!({"due": "2026-04-12T00:00:00.500Z"});

let warning = tasks_due_time_truncated_warning(&doc, &method, Some(&body));
assert!(warning.is_some());
}

#[test]
fn tasks_due_time_truncated_warning_ignores_non_tasks_apis() {
let doc = RestDescription {
name: "drive".to_string(),
..Default::default()
};
let method = RestMethod {
http_method: "POST".to_string(),
..Default::default()
};
let body = json!({"due": "2026-04-12T15:30:00.000Z"});

let warning = tasks_due_time_truncated_warning(&doc, &method, Some(&body));
assert!(warning.is_none());
}

#[test]
fn test_mime_to_extension_more_types() {
assert_eq!(mime_to_extension("text/plain"), "txt");
Expand Down
Loading