-
Notifications
You must be signed in to change notification settings - Fork 882
feat(auth): add --no-localhost flag to gws auth login
#231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9c3dc25
5e6f316
11e7bbd
f7e9a45
37091be
df65184
9654483
5a92570
210bee2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@googleworkspace/cli": minor | ||
| --- | ||
|
|
||
| feat(auth): add `--no-localhost` flag to `gws auth login` |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -140,6 +140,7 @@ pub async fn handle_auth_command(args: &[String]) -> Result<(), GwsError> { | |||||||||||||||||||||||||||||||||||||||||||||
| " --scopes Comma-separated custom scopes\n", | ||||||||||||||||||||||||||||||||||||||||||||||
| " -s, --services Comma-separated service names to limit scope picker\n", | ||||||||||||||||||||||||||||||||||||||||||||||
| " (e.g. -s drive,gmail,sheets)\n", | ||||||||||||||||||||||||||||||||||||||||||||||
| " --no-localhost Use out-of-band flow instead of starting a local server\n", | ||||||||||||||||||||||||||||||||||||||||||||||
| " setup Configure GCP project + OAuth client (requires gcloud)\n", | ||||||||||||||||||||||||||||||||||||||||||||||
| " --project Use a specific GCP project\n", | ||||||||||||||||||||||||||||||||||||||||||||||
| " status Show current authentication state\n", | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -183,7 +184,7 @@ impl yup_oauth2::authenticator_delegate::InstalledFlowDelegate for CliFlowDelega | |||||||||||||||||||||||||||||||||||||||||||||
| fn present_user_url<'a>( | ||||||||||||||||||||||||||||||||||||||||||||||
| &'a self, | ||||||||||||||||||||||||||||||||||||||||||||||
| url: &'a str, | ||||||||||||||||||||||||||||||||||||||||||||||
| _need_code: bool, | ||||||||||||||||||||||||||||||||||||||||||||||
| need_code: bool, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>> + Send + 'a>> | ||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||
| Box::pin(async move { | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -204,22 +205,59 @@ impl yup_oauth2::authenticator_delegate::InstalledFlowDelegate for CliFlowDelega | |||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
| eprintln!("Open this URL in your browser to authenticate:\n"); | ||||||||||||||||||||||||||||||||||||||||||||||
| eprintln!(" {display_url}\n"); | ||||||||||||||||||||||||||||||||||||||||||||||
| Ok(String::new()) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if need_code { | ||||||||||||||||||||||||||||||||||||||||||||||
| eprintln!("Enter the authorization code (or paste the full redirect URL):"); | ||||||||||||||||||||||||||||||||||||||||||||||
| let mut user_input = String::new(); | ||||||||||||||||||||||||||||||||||||||||||||||
| std::io::stdin() | ||||||||||||||||||||||||||||||||||||||||||||||
| .read_line(&mut user_input) | ||||||||||||||||||||||||||||||||||||||||||||||
| .map_err(|e| format!("Failed to read code: {e}"))?; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+211
to
+214
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of To fix this, you should perform the blocking I/O operation on a dedicated thread pool using
Suggested change
Comment on lines
+211
to
+214
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of To fix this, you should perform the blocking I/O operation on a dedicated blocking thread using
Suggested change
Comment on lines
+212
to
+214
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using While this might not cause a noticeable problem in the current context of a simple CLI prompt, it is a good practice to use non-blocking alternatives in Consider one of the following approaches:
Both solutions would require adding the necessary |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Ok(extract_code_from_input(&user_input)) | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| Ok(String::new()) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async fn handle_login(args: &[String]) -> Result<(), GwsError> { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Extract --account and -s/--services from args | ||||||||||||||||||||||||||||||||||||||||||||||
| /// Extracts the authorization code from user input. If the input is a full URL, | ||||||||||||||||||||||||||||||||||||||||||||||
| /// it parses the URL and extracts the `code` query parameter. Otherwise, it | ||||||||||||||||||||||||||||||||||||||||||||||
| /// assumes the entire input (trimmed) is the authorization code. | ||||||||||||||||||||||||||||||||||||||||||||||
| fn extract_code_from_input(input: &str) -> String { | ||||||||||||||||||||||||||||||||||||||||||||||
| let input = input.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||
| if let Ok(parsed_url) = reqwest::Url::parse(input) { | ||||||||||||||||||||||||||||||||||||||||||||||
| for (k, v) in parsed_url.query_pairs() { | ||||||||||||||||||||||||||||||||||||||||||||||
| if k == "code" { | ||||||||||||||||||||||||||||||||||||||||||||||
| return v.to_string(); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| input.to_string() | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| struct LoginArgs { | ||||||||||||||||||||||||||||||||||||||||||||||
| account_email: Option<String>, | ||||||||||||||||||||||||||||||||||||||||||||||
| services_filter: Option<HashSet<String>>, | ||||||||||||||||||||||||||||||||||||||||||||||
| no_localhost: bool, | ||||||||||||||||||||||||||||||||||||||||||||||
| filtered_args: Vec<String>, | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| fn parse_login_args(args: &[String]) -> LoginArgs { | ||||||||||||||||||||||||||||||||||||||||||||||
| let mut account_email: Option<String> = None; | ||||||||||||||||||||||||||||||||||||||||||||||
| let mut services_filter: Option<HashSet<String>> = None; | ||||||||||||||||||||||||||||||||||||||||||||||
| let mut no_localhost = false; | ||||||||||||||||||||||||||||||||||||||||||||||
| let mut filtered_args: Vec<String> = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||
| let mut skip_next = false; | ||||||||||||||||||||||||||||||||||||||||||||||
| for i in 0..args.len() { | ||||||||||||||||||||||||||||||||||||||||||||||
| if skip_next { | ||||||||||||||||||||||||||||||||||||||||||||||
| skip_next = false; | ||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if args[i] == "--no-localhost" { | ||||||||||||||||||||||||||||||||||||||||||||||
| no_localhost = true; | ||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+257
to
+260
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The manual argument parsing logic in this function has a bug that affects this new flag. If a user provides an argument-taking flag like For example: This will result in The root cause is that argument-taking flags don't check if the next token is another flag. While a full refactor to use A potential fix for the if args[i] == "--account" && i + 1 < args.len() && !args[i + 1].starts_with('-') {
// ...
}Since this change would be outside the diff, I'm pointing out the issue here on the new code that is impacted by it. |
||||||||||||||||||||||||||||||||||||||||||||||
| if args[i] == "--account" && i + 1 < args.len() { | ||||||||||||||||||||||||||||||||||||||||||||||
| account_email = Some(args[i + 1].clone()); | ||||||||||||||||||||||||||||||||||||||||||||||
| skip_next = true; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -249,6 +287,21 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { | |||||||||||||||||||||||||||||||||||||||||||||
| filtered_args.push(args[i].clone()); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| LoginArgs { | ||||||||||||||||||||||||||||||||||||||||||||||
| account_email, | ||||||||||||||||||||||||||||||||||||||||||||||
| services_filter, | ||||||||||||||||||||||||||||||||||||||||||||||
| no_localhost, | ||||||||||||||||||||||||||||||||||||||||||||||
| filtered_args, | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async fn handle_login(args: &[String]) -> Result<(), GwsError> { | ||||||||||||||||||||||||||||||||||||||||||||||
| let parsed = parse_login_args(args); | ||||||||||||||||||||||||||||||||||||||||||||||
| let account_email = parsed.account_email; | ||||||||||||||||||||||||||||||||||||||||||||||
| let services_filter = parsed.services_filter; | ||||||||||||||||||||||||||||||||||||||||||||||
| let no_localhost = parsed.no_localhost; | ||||||||||||||||||||||||||||||||||||||||||||||
| let filtered_args = parsed.filtered_args; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Resolve client_id and client_secret: | ||||||||||||||||||||||||||||||||||||||||||||||
| // 1. Env vars (highest priority) | ||||||||||||||||||||||||||||||||||||||||||||||
| // 2. Saved client_secret.json from `gws auth setup` or manual download | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -278,12 +331,24 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { | |||||||||||||||||||||||||||||||||||||||||||||
| // are already included. | ||||||||||||||||||||||||||||||||||||||||||||||
| let mut scopes = filter_redundant_restrictive_scopes(scopes); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let redirect_uris = if no_localhost { | ||||||||||||||||||||||||||||||||||||||||||||||
| vec![ | ||||||||||||||||||||||||||||||||||||||||||||||
| "urn:ietf:wg:oauth:2.0:oob".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| "http://localhost".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| vec![ | ||||||||||||||||||||||||||||||||||||||||||||||
| "http://localhost".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| "urn:ietf:wg:oauth:2.0:oob".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
jpoehnelt marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+334
to
+344
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of code appears to have a copy-paste error, resulting in duplicated logic and a syntax error that will prevent compilation. The let redirect_uris = if no_localhost {
vec!["urn:ietf:wg:oauth:2.0:oob".to_string()]
} else {
vec!["http://localhost".to_string()]
}; |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let secret = yup_oauth2::ApplicationSecret { | ||||||||||||||||||||||||||||||||||||||||||||||
| client_id: client_id.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||
| client_secret: client_secret.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||
| auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| token_uri: "https://oauth2.googleapis.com/token".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| redirect_uris: vec!["http://localhost".to_string()], | ||||||||||||||||||||||||||||||||||||||||||||||
| redirect_uris, | ||||||||||||||||||||||||||||||||||||||||||||||
| ..Default::default() | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -310,20 +375,23 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { | |||||||||||||||||||||||||||||||||||||||||||||
| .map_err(|e| GwsError::Validation(format!("Failed to create config directory: {e}")))?; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let auth = yup_oauth2::InstalledFlowAuthenticator::builder( | ||||||||||||||||||||||||||||||||||||||||||||||
| secret, | ||||||||||||||||||||||||||||||||||||||||||||||
| yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| .with_storage(Box::new(crate::token_storage::EncryptedTokenStorage::new( | ||||||||||||||||||||||||||||||||||||||||||||||
| temp_path.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||
| ))) | ||||||||||||||||||||||||||||||||||||||||||||||
| .force_account_selection(true) // Adds prompt=consent so Google always returns a refresh_token | ||||||||||||||||||||||||||||||||||||||||||||||
| .flow_delegate(Box::new(CliFlowDelegate { | ||||||||||||||||||||||||||||||||||||||||||||||
| login_hint: account_email.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||
| .build() | ||||||||||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||||||||||
| .map_err(|e| GwsError::Auth(format!("Failed to build authenticator: {e}")))?; | ||||||||||||||||||||||||||||||||||||||||||||||
| let return_method = if no_localhost { | ||||||||||||||||||||||||||||||||||||||||||||||
| yup_oauth2::InstalledFlowReturnMethod::Interactive | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let auth = yup_oauth2::InstalledFlowAuthenticator::builder(secret, return_method) | ||||||||||||||||||||||||||||||||||||||||||||||
| .with_storage(Box::new(crate::token_storage::EncryptedTokenStorage::new( | ||||||||||||||||||||||||||||||||||||||||||||||
| temp_path.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||
| ))) | ||||||||||||||||||||||||||||||||||||||||||||||
| .force_account_selection(true) // Adds prompt=consent so Google always returns a refresh_token | ||||||||||||||||||||||||||||||||||||||||||||||
| .flow_delegate(Box::new(CliFlowDelegate { | ||||||||||||||||||||||||||||||||||||||||||||||
| login_hint: account_email.clone(), | ||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||
| .build() | ||||||||||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||||||||||
| .map_err(|e| GwsError::Auth(format!("Failed to build authenticator: {e}")))?; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Request a token — this triggers the browser OAuth flow | ||||||||||||||||||||||||||||||||||||||||||||||
| let scope_refs: Vec<&str> = scopes.iter().map(|s| s.as_str()).collect(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2184,4 +2252,74 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||||
| // Exactly 9 chars — first 4 + last 4 with "..." in between | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(mask_secret("123456789"), "1234...6789"); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||
| fn test_extract_code_from_input_raw_code() { | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(extract_code_from_input("4/0Aea..."), "4/0Aea..."); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(extract_code_from_input(" 4/0Aea... \n"), "4/0Aea..."); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||
| fn test_extract_code_from_input_url() { | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||
| extract_code_from_input("http://localhost/?code=4/0Aea...&scope=email"), | ||||||||||||||||||||||||||||||||||||||||||||||
| "4/0Aea..." | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||
| extract_code_from_input("urn:ietf:wg:oauth:2.0:oob?code=my-secret-code"), | ||||||||||||||||||||||||||||||||||||||||||||||
| "my-secret-code" | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||
| fn test_extract_code_from_input_url_no_code() { | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||
| extract_code_from_input("http://localhost/?error=access_denied"), | ||||||||||||||||||||||||||||||||||||||||||||||
| "http://localhost/?error=access_denied" | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||
| fn test_parse_login_args_no_localhost() { | ||||||||||||||||||||||||||||||||||||||||||||||
| let args = vec![ | ||||||||||||||||||||||||||||||||||||||||||||||
| "--no-localhost".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| "--scopes".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| "drive".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||
| let parsed = parse_login_args(&args); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert!(parsed.no_localhost); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parsed.filtered_args, vec!["--scopes", "drive"]); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||
| fn test_parse_login_args_account() { | ||||||||||||||||||||||||||||||||||||||||||||||
| let args = vec![ | ||||||||||||||||||||||||||||||||||||||||||||||
| "--account".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| "test@example.com".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| "--no-localhost".to_string(), | ||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||
| let parsed = parse_login_args(&args); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parsed.account_email.unwrap(), "test@example.com"); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert!(parsed.no_localhost); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert!(parsed.filtered_args.is_empty()); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let args2 = vec!["--account=test2@example.com".to_string()]; | ||||||||||||||||||||||||||||||||||||||||||||||
| let parsed2 = parse_login_args(&args2); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parsed2.account_email.unwrap(), "test2@example.com"); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||
| fn test_parse_login_args_services() { | ||||||||||||||||||||||||||||||||||||||||||||||
| let args = vec!["-s".to_string(), "drive,gmail".to_string()]; | ||||||||||||||||||||||||||||||||||||||||||||||
| let parsed = parse_login_args(&args); | ||||||||||||||||||||||||||||||||||||||||||||||
| let filter = parsed.services_filter.unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert!(filter.contains("drive")); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert!(filter.contains("gmail")); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert!(!parsed.no_localhost); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| let args2 = vec!["--services=sheets".to_string()]; | ||||||||||||||||||||||||||||||||||||||||||||||
| let parsed2 = parse_login_args(&args2); | ||||||||||||||||||||||||||||||||||||||||||||||
| let filter2 = parsed2.services_filter.unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||
| assert!(filter2.contains("sheets")); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of
std::io::stdin().read_line()here is a blocking I/O operation inside anasyncblock. This can block the entire worker thread of the async runtime, leading to poor performance and unresponsiveness, especially if other async tasks are pending. It's crucial to avoid blocking calls in async code.To fix this, you should move the blocking operation to a dedicated thread pool using
tokio::task::spawn_blocking.