diff --git a/.changeset/trim-oauth-credentials.md b/.changeset/trim-oauth-credentials.md new file mode 100644 index 000000000..9bbb02f54 --- /dev/null +++ b/.changeset/trim-oauth-credentials.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Trim pasted OAuth client IDs and secrets during interactive setup so invisible surrounding whitespace does not cause authentication failures. diff --git a/crates/google-workspace-cli/src/setup.rs b/crates/google-workspace-cli/src/setup.rs index 9ebd19cb5..38e64c9cf 100644 --- a/crates/google-workspace-cli/src/setup.rs +++ b/crates/google-workspace-cli/src/setup.rs @@ -1473,6 +1473,16 @@ fn manual_oauth_instructions(project_id: &str) -> String { ) } +fn normalize_oauth_credential(value: String, field_name: &str) -> Result { + let value = value.trim().to_string(); + if value.is_empty() { + return Err(GwsError::Validation(format!( + "{field_name} cannot be empty" + ))); + } + Ok(value) +} + /// Stage 5: Configure OAuth consent screen and collect client credentials. async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result { ctx.wiz(4, StepStatus::InProgress("Configuring...".into())); @@ -1521,7 +1531,7 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result w + crate::setup_tui::InputResult::Confirmed(v) if !v.trim().is_empty() => w .show_input( "Enter OAuth Client Secret", "Paste the Client Secret from Google Cloud Console", @@ -1541,11 +1551,7 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result { - if v.is_empty() { - ctx.finish_wizard(); - return Err(GwsError::Validation("Client ID cannot be empty".into())); - } - v + normalize_oauth_credential(v, "Client ID").inspect_err(|_| ctx.finish_wizard())? } crate::setup_tui::InputResult::GoBack => { return Ok(SetupStage::EnableApis); @@ -1558,11 +1564,7 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result { - if v.is_empty() { - ctx.finish_wizard(); - return Err(GwsError::Validation("Client Secret cannot be empty".into())); - } - v + normalize_oauth_credential(v, "Client Secret").inspect_err(|_| ctx.finish_wizard())? } crate::setup_tui::InputResult::GoBack => { return Ok(SetupStage::EnableApis); @@ -1930,6 +1932,31 @@ mod tests { assert!(!should_offer_login_prompt(true, true, false, true)); } + #[test] + fn test_normalize_oauth_credential_trims_surrounding_whitespace() { + assert_eq!( + normalize_oauth_credential(" client-id\n".into(), "Client ID").unwrap(), + "client-id" + ); + assert_eq!( + normalize_oauth_credential("\tclient secret ".into(), "Client Secret").unwrap(), + "client secret" + ); + } + + #[test] + fn test_normalize_oauth_credential_rejects_whitespace_only_input() { + let client_id_error = normalize_oauth_credential(" \t\n".into(), "Client ID").unwrap_err(); + assert_eq!(client_id_error.to_string(), "Client ID cannot be empty"); + + let client_secret_error = + normalize_oauth_credential("\n ".into(), "Client Secret").unwrap_err(); + assert_eq!( + client_secret_error.to_string(), + "Client Secret cannot be empty" + ); + } + #[test] fn test_format_project_create_failure_tos_guidance() { let msg = format_project_create_failure(