diff --git a/src/cli.rs b/src/cli.rs index 6e2eda6..0f245a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,11 +22,11 @@ pub struct Cli { #[derive(Args)] pub struct CipherArgs { /// Text to process - #[arg(short, long)] + #[arg(short, long, conflicts_with = "file")] pub text: Option, /// Input file path - #[arg(short = 'f', long)] + #[arg(short = 'f', long, conflicts_with = "text")] pub file: Option, /// Shift value (any integer; safe mode: -25 to 25, default: 3) @@ -57,11 +57,11 @@ pub enum Commands { /// Show all possible decryptions (brute force) BruteForce { /// Text to decrypt - #[arg(short, long)] + #[arg(short, long, conflicts_with = "file")] text: Option, /// Input file path - #[arg(short = 'f', long)] + #[arg(short = 'f', long, conflicts_with = "text")] file: Option, }, } @@ -136,52 +136,47 @@ pub fn run_cli() -> Result<(), Box> { /// # Errors /// /// Returns an error if: -/// - Both text and file are provided simultaneously /// - File reading fails /// - Stdin reading fails fn get_input_text( text: Option, file: Option, ) -> Result> { - match (text, file) { - (Some(t), None) => { - if t.len() > MAX_INPUT_SIZE { - return Err(format!( - "Input text exceeds maximum size of {} bytes", - MAX_INPUT_SIZE - ) - .into()); - } - Ok(t) + if let Some(t) = text { + if t.len() > MAX_INPUT_SIZE { + return Err(format!( + "Input text exceeds maximum size of {} bytes", + MAX_INPUT_SIZE + ) + .into()); } - (None, Some(f)) => { - let metadata = - fs::metadata(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e))?; - if metadata.len() > MAX_INPUT_SIZE as u64 { - return Err(format!( - "Input file '{}' exceeds maximum size of {} bytes", - f, MAX_INPUT_SIZE - ) - .into()); - } - fs::read_to_string(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e).into()) - } - (Some(_), Some(_)) => Err("Cannot specify both text and file".into()), - (None, None) => { - print!("Enter text: "); - io::stdout().flush()?; - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - if input.len() > MAX_INPUT_SIZE { - return Err(format!( - "Input text exceeds maximum size of {} bytes", - MAX_INPUT_SIZE - ) - .into()); - } - Ok(trim_trailing_newline(&input).to_string()) + return Ok(t); + } + + if let Some(f) = file { + let metadata = fs::metadata(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e))?; + if metadata.len() > MAX_INPUT_SIZE as u64 { + return Err(format!( + "Input file '{}' exceeds maximum size of {} bytes", + f, MAX_INPUT_SIZE + ) + .into()); } + return fs::read_to_string(&f).map_err(|e| format!("Failed to read file '{}': {}", f, e).into()); + } + + print!("Enter text: "); + io::stdout().flush()?; + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + if input.len() > MAX_INPUT_SIZE { + return Err(format!( + "Input text exceeds maximum size of {} bytes", + MAX_INPUT_SIZE + ) + .into()); } + Ok(input.trim().to_string()) } fn trim_trailing_newline(input: &str) -> &str { @@ -391,13 +386,10 @@ mod tests { } #[test] - fn test_get_input_text_both_provided() { + fn test_get_input_text_prefers_text_when_both_provided() { let result = get_input_text(Some("Hello".to_string()), Some("file.txt".to_string())); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Cannot specify both text and file")); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "Hello"); } #[test] diff --git a/tests/cli_output_tests.rs b/tests/cli_output_tests.rs index 92a03b0..c4b850b 100644 --- a/tests/cli_output_tests.rs +++ b/tests/cli_output_tests.rs @@ -7,7 +7,7 @@ //! | テスト観点 | 分類 | 期待値 | //! |----------------------------------|----------|---------------------------------------| //! | 存在しないファイル読込 | 異常系 | エラーメッセージにファイルパス含む | -//! | text+file同時指定 | 異常系 | "Cannot specify both" エラー | +//! | text+file同時指定 | 異常系 | clap が即エラーを返す | //! | テキスト引数のみ指定 | 正常系 | テキストがそのまま返る | //! | ファイルから正常読込 | 正常系 | ファイル内容が返る | //! | 空ファイル読込 | 境界値 | 空文字列返却 | @@ -180,10 +180,43 @@ fn test_cli_both_text_and_file_error() { let stderr = String::from_utf8_lossy(&output.stderr); - // Then: Error message mentions the conflict + // Then: clap validates conflict before application logic + assert!(!output.status.success(), "Command should fail on clap validation"); assert!( - stderr.contains("Cannot specify both"), - "Error should mention 'Cannot specify both', got: {}", + stderr.contains("cannot be used with") || stderr.contains("conflicts with"), + "Error should mention clap conflict, got: {}", + stderr + ); +} + +#[test] +fn test_cli_brute_force_both_text_and_file_error() { + // Given: Both text and file arguments provided for brute-force + let mut temp_file = NamedTempFile::new().unwrap(); + write!(temp_file, "content").unwrap(); + let file_path = temp_file.path().to_string_lossy().to_string(); + + // When: CLI receives both arguments + let output = std::process::Command::new("cargo") + .args([ + "run", + "--", + "brute-force", + "--text", + "Khoor", + "--file", + &file_path, + ]) + .output() + .expect("Failed to execute CLI"); + + let stderr = String::from_utf8_lossy(&output.stderr); + + // Then: clap validates conflict before application logic + assert!(!output.status.success(), "Command should fail on clap validation"); + assert!( + stderr.contains("cannot be used with") || stderr.contains("conflicts with"), + "Error should mention clap conflict, got: {}", stderr ); }