diff --git a/src/cortex-cli/src/login.rs b/src/cortex-cli/src/login.rs index 138451fc..c3499247 100644 --- a/src/cortex-cli/src/login.rs +++ b/src/cortex-cli/src/login.rs @@ -7,7 +7,7 @@ use cortex_login::{ safe_format_key, save_auth_with_fallback, }; use std::collections::HashSet; -use std::io::{IsTerminal, Read}; +use std::io::{BufRead, IsTerminal, Read}; use std::path::PathBuf; /// Check for duplicate config override keys and warn the user. @@ -41,6 +41,14 @@ fn get_cortex_home() -> PathBuf { }) } +/// Read and parse a logout confirmation response. +pub fn read_logout_confirmation(reader: &mut R) -> std::io::Result { + let mut input = String::new(); + reader.read_line(&mut input)?; + let input = input.trim().to_lowercase(); + Ok(input == "y" || input == "yes") +} + /// Run login with API key. pub async fn run_login_with_api_key(config_overrides: CliConfigOverrides, api_key: String) -> ! { check_duplicate_config_overrides(&config_overrides); @@ -205,13 +213,18 @@ pub async fn run_logout(config_overrides: CliConfigOverrides, skip_confirmation: ); let _ = std::io::Write::flush(&mut std::io::stderr()); - let mut input = String::new(); - if std::io::stdin().read_line(&mut input).is_ok() { - let input = input.trim().to_lowercase(); - if input != "y" && input != "yes" { + let stdin = std::io::stdin(); + let mut stdin = stdin.lock(); + match read_logout_confirmation(&mut stdin) { + Ok(true) => {} + Ok(false) => { print_info("Logout cancelled."); std::process::exit(0); } + Err(e) => { + print_error(&format!("Failed to read logout confirmation: {e}")); + std::process::exit(1); + } } } } diff --git a/src/cortex-cli/tests/logout_confirmation.rs b/src/cortex-cli/tests/logout_confirmation.rs new file mode 100644 index 00000000..fe15c245 --- /dev/null +++ b/src/cortex-cli/tests/logout_confirmation.rs @@ -0,0 +1,44 @@ +use std::io::{self, BufRead, Cursor, Read}; + +use cortex_cli::login::read_logout_confirmation; + +struct FailingReader; + +impl Read for FailingReader { + fn read(&mut self, _buf: &mut [u8]) -> io::Result { + Err(io::Error::other("stdin unavailable")) + } +} + +impl BufRead for FailingReader { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + Err(io::Error::other("stdin unavailable")) + } + + fn consume(&mut self, _amt: usize) {} +} + +#[test] +fn logout_confirmation_accepts_yes_values() { + let mut lowercase = Cursor::new(b"yes\n"); + assert!(read_logout_confirmation(&mut lowercase).unwrap()); + + let mut uppercase = Cursor::new(b"Y\n"); + assert!(read_logout_confirmation(&mut uppercase).unwrap()); +} + +#[test] +fn logout_confirmation_rejects_empty_or_negative_values() { + let mut empty = Cursor::new(b"\n"); + assert!(!read_logout_confirmation(&mut empty).unwrap()); + + let mut no = Cursor::new(b"no\n"); + assert!(!read_logout_confirmation(&mut no).unwrap()); +} + +#[test] +fn logout_confirmation_propagates_read_errors() { + let mut reader = FailingReader; + let error = read_logout_confirmation(&mut reader).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::Other); +}