use anyhow::Result;
#[allow(unused_imports)] use predicates::prelude::*;
use tempfile::NamedTempFile;
use std::io::Write;
use std::fs;
#[allow(unused_imports)] use assert_cmd::prelude::*;
use assert_cmd::Command;
use strip_ansi_escapes::strip as strip_ansi_escapes_fn;
fn run_cleansh_command(input: &str, args: &[&str]) -> assert_cmd::assert::Assert {
let mut cmd = Command::new(assert_cmd::cargo_bin!("cleansh"));
cmd.env("RUST_LOG", "debug");
cmd.env("CLEANSH_ALLOW_DEBUG_PII", "true");
cmd.args(args);
cmd.write_stdin(input.as_bytes()).unwrap();
cmd.assert()
}
fn strip_ansi(s: &str) -> String {
let cleaned = strip_ansi_escapes_fn(s);
String::from_utf8_lossy(&cleaned).to_string()
}
#[test]
fn test_basic_sanitization() -> Result<()> {
let input = "My email is test@example.com and my IP is 192.168.1.1.";
let expected_stdout = "My email is [EMAIL_REDACTED] and my IP is [IPV4_REDACTED].\n";
let expected_stderr_contains_substrings = vec![
"[INFO cleansh] cleansh started. Version: 0.1.9".to_string(),
"[DEBUG cleansh_core::config] Loading default rules from embedded string...".to_string(),
"[DEBUG cleansh_core::sanitizers::compiler] Rule 'email' compiled successfully.".to_string(),
"[DEBUG cleansh_core::sanitizers::compiler] Rule 'ipv4_address' compiled successfully.".to_string(),
"Reading input from stdin...".to_string(),
"[INFO cleansh::commands::cleansh] Starting cleansh operation.".to_string(),
"Writing sanitized content to stdout.".to_string(),
"Displaying redaction summary.".to_string(),
"--- Redaction Summary ---".to_string(),
"ipv4_address (1 occurrences)".to_string(),
"email (1 occurrences)".to_string(),
"[INFO cleansh::commands::cleansh] Cleansh operation completed.".to_string(),
];
let assert_result = run_cleansh_command(input, &["sanitize"]).success();
let stdout = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stdout));
let stderr = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stderr));
eprint!("\n--- STDOUT Captured ---\n");
eprintln!("{}", stdout);
eprintln!("--- END STDOUT ---\n");
eprint!("\n--- STDERR Captured ---\n");
eprintln!("{}", stderr);
eprintln!("--- END STDERR ---\n");
assert_eq!(stdout, expected_stdout);
for msg in expected_stderr_contains_substrings {
assert!(stderr.contains(&msg), "Stderr missing: '{}'\nFull stderr:\n{}", msg, stderr);
}
assert!(
stderr.contains("[DEBUG cleansh_core::redaction_match] cleansh_core::engine Captured match (original): 'test@example.com' for rule 'email'"),
"Stderr missing expected original capture log for email.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains("[DEBUG cleansh_core::redaction_match] cleansh_core::engine Captured match (original): '192.168.1.1' for rule 'ipv4_address'"),
"Stderr missing expected original capture log for IP.\nFull stderr:\n{}", stderr
);
Ok(())
}
#[cfg(feature = "clipboard")]
#[test]
fn test_run_cleansh_clipboard_copy_to_file() -> Result<()> {
if std::env::var("CI").is_ok() {
eprintln!("Skipping clipboard test in CI (no display)");
return Ok(());
}
let input = "My email is test@example.com";
let expected_file_content = "My email is [EMAIL_REDACTED]\n";
let config_yaml = r#"rules:
- name: "email"
pattern: "([a-z]+@[a-z]+\\.com)"
replace_with: "[EMAIL_REDACTED]"
description: "Email address."
multiline: false
dot_matches_new_line: false
programmatic_validation: false
opt_in: false
"#;
let mut config_file = NamedTempFile::new()?;
config_file.write_all(config_yaml.as_bytes())?;
let config_path = config_file.path().to_str().unwrap();
let output_file = NamedTempFile::new()?;
let output_path = output_file.path().to_str().unwrap();
let assert_result = run_cleansh_command(input, &[
"sanitize", "-c", "-o", output_path, "--config", config_path, "--no-redaction-summary", ]).success();
let stdout = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stdout));
let stderr = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stderr));
eprint!("\n--- STDOUT Captured ---\n");
eprintln!("{}", stdout);
eprintln!("--- END STDOUT ---\n");
eprint!("\n--- STDERR Captured ---\n");
eprintln!("{}", stderr);
eprintln!("--- END STDERR ---\n");
assert_eq!(stdout, "");
assert!(
stderr.contains("[INFO cleansh] cleansh started. Version: 0.1.9"),
"Stderr missing `cleansh started` log.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains("Reading input from stdin..."),
"Stderr missing `Reading input` log.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains("[INFO cleansh::commands::cleansh] Starting cleansh operation."),
"Stderr missing `Starting cleansh operation` log.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains("Writing sanitized content to file:"),
"Stderr missing `Writing sanitized content to file:` log.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains(&format!("[DEBUG cleansh::commands::cleansh] [cleansh::commands::cleansh] Outputting to file: {}", output_path)),
"Stderr missing `Outputting to file:` log with path.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains("Sanitized content copied to clipboard successfully."),
"Stderr missing `Sanitized content copied to clipboard` log.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains("[DEBUG cleansh_core::redaction_match] cleansh_core::engine Captured match (original): 'test@example.com' for rule 'email'"),
"Stderr missing `Captured match (original)` log.\nFull stderr:\n{}", stderr
);
assert!(
stderr.contains("[INFO cleansh::commands::cleansh] Cleansh operation completed."),
"Stderr missing `Cleansh operation completed` log.\nFull stderr:\n{}", stderr
);
let file_contents = fs::read_to_string(output_path)?;
assert_eq!(file_contents, expected_file_content);
Ok(())
}
#[cfg(feature = "clipboard")]
#[test]
fn test_clipboard_output_with_jwt() -> Result<()> {
if std::env::var("CI").is_ok() {
eprintln!("Skipping clipboard test in CI (no display)");
return Ok(());
}
let input = "Secret JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let expected_stdout = "Secret JWT: [JWT_REDACTED]\n";
let expected_stderr_contains_substrings = vec![
"Reading input from stdin...".to_string(),
"Sanitized content copied to clipboard successfully.".to_string(),
"[INFO cleansh::commands::cleansh] Starting cleansh operation.".to_string(),
"[DEBUG cleansh_core::config] Loading default rules from embedded string...".to_string(),
"[DEBUG cleansh_core::sanitizers::compiler] Rule 'jwt_token' compiled successfully.".to_string(),
"[DEBUG cleansh_core::redaction_match] cleansh_core::engine Captured match (original): 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' for rule 'jwt_token'".to_string(),
"[INFO cleansh::commands::cleansh] Cleansh operation completed.".to_string(),
];
let assert_result = run_cleansh_command(input, &["sanitize", "--clipboard", "--no-redaction-summary"]).success();
let stdout = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stdout));
let stderr = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stderr));
eprint!("\n--- STDOUT Captured ---\n");
eprintln!("{}", stdout);
eprintln!("--- END STDOUT ---\n");
eprint!("\n--- STDERR Captured ---\n");
eprintln!("{}", stderr);
eprintln!("--- END STDERR ---\n");
assert_eq!(stdout, expected_stdout);
for msg in expected_stderr_contains_substrings {
assert!(stderr.contains(&msg), "Stderr missing: '{}'\nFull stderr:\n{}", msg, stderr);
}
Ok(())
}
#[test]
fn test_diff_view() -> Result<()> {
let input = "Old IP: 10.0.0.1. New IP: 192.168.1.1.";
let assert_result = run_cleansh_command(input, &["sanitize", "--diff", "--no-redaction-summary"]).success();
let stdout = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stdout));
assert!(stdout.contains("-Old IP: 10.0.0.1. New IP: 192.168.1.1.\n"));
assert!(stdout.contains("+Old IP: [IPV4_REDACTED]. New IP: [IPV4_REDACTED].\n"));
Ok(())
}
#[test]
fn test_output_to_file() -> Result<()> {
let file = NamedTempFile::new()?;
let file_path = file.path().to_owned();
let file_path_str = file_path.to_str().unwrap();
let input = "This is a test with sensitive info: user@domain.com";
let assert_result = run_cleansh_command(input, &["sanitize", "-o", file_path_str, "--no-redaction-summary"]).success();
let stdout = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stdout));
assert_eq!(stdout, "");
let content = fs::read_to_string(&file_path)?;
assert_eq!(content, "This is a test with sensitive info: [EMAIL_REDACTED]\n");
Ok(())
}
#[test]
fn test_custom_config_file() -> Result<()> {
let mut config_file = NamedTempFile::new()?;
let config_content = r#"
rules:
- name: "my_secret_token"
pattern: "MYSECRET-\\d{4}"
replace_with: "[SECRET_TOKEN]"
opt_in: false
description: "A secret token for testing"
multiline: false
dot_matches_new_line: false
- name: "email"
pattern: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b"
replace_with: "[EMAIL_REDACTED]"
opt_in: false
description: "Email Address"
multiline: false
dot_matches_new_line: false
"#;
config_file.write_all(config_content.as_bytes())?;
let config_path = config_file.path().to_str().unwrap();
let original_text =
"My email is user@example.com and another is user@test.org. My secret is MYSECRET-1234.";
let assert_result = run_cleansh_command(original_text, &["sanitize", "--config", config_path, "--no-redaction-summary"]).success();
let stdout = strip_ansi(&String::from_utf8_lossy(&assert_result.get_output().stdout));
assert_eq!(stdout, "My email is [EMAIL_REDACTED] and another is [EMAIL_REDACTED]. My secret is [SECRET_TOKEN].\n");
Ok(())
}