use crate::utils::redact_tool_input;
use crate::utils::sanitize::redact_secrets;
#[test]
fn redacts_env_var_pass_assignment() {
let input = r#"ADMIN_PASS="fuNZEIYc2isz0txisiWTKg8A""#;
let out = redact_secrets(input);
assert!(
!out.contains("fuNZEIYc2isz0txisiWTKg8A"),
"password leaked: {out}"
);
assert!(
out.contains("ADMIN_PASS="),
"variable name should be preserved: {out}"
);
}
#[test]
fn redacts_env_var_secret_assignment() {
let input = "NEW_SECRET=mgd4EjM8oTrmvWPEbqKys7q2c5H6N7";
let out = redact_secrets(input);
assert!(
!out.contains("mgd4EjM8oTrmvWPEbqKys7q2c5H6N7"),
"secret leaked: {out}"
);
}
#[test]
fn redacts_env_var_token_assignment() {
let input = "API_TOKEN=abc123def456ghi789jkl012mno345";
let out = redact_secrets(input);
assert!(
!out.contains("abc123def456ghi789jkl012mno345"),
"token leaked: {out}"
);
}
#[test]
fn redacts_env_var_apikey_assignment() {
let input = "MY_APIKEY=sk_live_abcdef1234567890abcdef";
let out = redact_secrets(input);
assert!(
!out.contains("abcdef1234567890abcdef"),
"api key leaked: {out}"
);
}
#[test]
fn redacts_env_var_credential_assignment() {
let input = "DB_CREDENTIAL=super_secret_password_12345";
let out = redact_secrets(input);
assert!(
!out.contains("super_secret_password_12345"),
"credential leaked: {out}"
);
}
#[test]
fn redacts_env_var_auth_assignment() {
let input = "SERVICE_AUTH=bearer_token_abcdefghijklmnop";
let out = redact_secrets(input);
assert!(
!out.contains("bearer_token_abcdefghijklmnop"),
"auth value leaked: {out}"
);
}
#[test]
fn redacts_piped_secret_double_quotes() {
let input =
r#"echo "mgd4EjM8oTrmvWPEbqKys7q2c5H6N7" | docker login -u robot$harbor --password-stdin"#;
let out = redact_secrets(input);
assert!(
!out.contains("mgd4EjM8oTrmvWPEbqKys7q2c5H6N7"),
"piped secret leaked: {out}"
);
assert!(out.contains("echo"), "echo command preserved: {out}");
assert!(
out.contains("docker login"),
"docker command preserved: {out}"
);
}
#[test]
fn redacts_piped_secret_single_quotes() {
let input = "echo 'superSecretToken1234567890ab' | kubectl apply -f -";
let out = redact_secrets(input);
assert!(
!out.contains("superSecretToken1234567890ab"),
"piped secret leaked: {out}"
);
}
#[test]
fn does_not_redact_short_echo_values() {
let input = r#"echo "hello" | cat"#;
let out = redact_secrets(input);
assert!(
out.contains("hello"),
"short non-secret should not be redacted: {out}"
);
}
#[test]
fn redacts_server_ip() {
let input = "Connected to 198.51.100.23 on port 443";
let out = redact_secrets(input);
assert!(!out.contains("198.51.100.23"), "server IP leaked: {out}");
assert!(
out.contains("[IP_REDACTED]"),
"IP should be replaced with [IP_REDACTED]: {out}"
);
}
#[test]
fn redacts_multiple_ips() {
let input = "Primary: 10.0.1.5, Secondary: 192.168.1.100";
let out = redact_secrets(input);
assert!(!out.contains("10.0.1.5"), "first IP leaked: {out}");
assert!(!out.contains("192.168.1.100"), "second IP leaked: {out}");
}
#[test]
fn preserves_localhost() {
let input = "Listening on 127.0.0.1:8080";
let out = redact_secrets(input);
assert!(
out.contains("127.0.0.1"),
"localhost should be preserved: {out}"
);
}
#[test]
fn preserves_zero_address() {
let input = "Binding to 0.0.0.0:3000";
let out = redact_secrets(input);
assert!(
out.contains("0.0.0.0"),
"0.0.0.0 should be preserved: {out}"
);
}
#[test]
fn redacts_bearer_token() {
let input = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test";
let out = redact_secrets(input);
assert!(
!out.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
"JWT leaked: {out}"
);
}
#[test]
fn redacts_api_key_assignment() {
let input = "api_key=sk-proj-abcdef1234567890abcdef1234567890";
let out = redact_secrets(input);
assert!(
!out.contains("abcdef1234567890abcdef1234567890"),
"API key leaked: {out}"
);
}
#[test]
fn redacts_github_pat() {
let input = "Token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12";
let out = redact_secrets(input);
assert!(
!out.contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12"),
"GitHub PAT leaked: {out}"
);
}
#[test]
fn redacts_long_hex_token() {
let input = "secret: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6";
let out = redact_secrets(input);
assert!(
!out.contains("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"),
"hex token leaked: {out}"
);
}
#[test]
fn redacts_mixed_output_with_ips_and_secrets() {
let input = r#"Deploying to 203.0.113.4 with ADMIN_PASS="EXAMPLE_pw_a1b2c3d4e5" and token ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12"#;
let out = redact_secrets(input);
assert!(!out.contains("203.0.113.4"), "IP leaked: {out}");
assert!(
!out.contains("EXAMPLE_pw_a1b2c3d4e5"),
"password leaked: {out}"
);
assert!(
!out.contains("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef12"),
"GitHub PAT leaked: {out}"
);
}
#[test]
fn preserves_non_sensitive_output() {
let input = "Build completed successfully in 42 seconds with 0 errors";
let out = redact_secrets(input);
assert_eq!(out, input, "non-sensitive output should be unchanged");
}
#[test]
fn does_not_panic_on_multibyte_char_after_prefix_match() {
let input = "sk-x → next text here that follows the arrow → and more arrows →→→";
let _out = redact_secrets(input); }
#[test]
fn handles_multibyte_chars_at_various_offsets_after_prefix() {
for pad_bytes in 0..=15 {
let padding: String = "x".repeat(pad_bytes);
let input = format!("sk-{padding}→ rest of text continues");
let _out = redact_secrets(&input); }
}
#[test]
fn cyrillic_emoji_cjk_in_post_prefix_window_do_not_panic() {
let inputs = ["sk-z я тест", "sk-z 中文测试", "sk-z 🦀🦀🦀 опенкрабс"];
for input in inputs {
let _out = redact_secrets(input); }
}
#[test]
fn redact_secrets_query_param_api_key() {
let text = "fetch failed: https://api.example.com/v1/sync?api_key=sk-shortABC123&page=2";
let out = redact_secrets(text);
assert!(
!out.contains("sk-shortABC123"),
"api_key query param leaked: {out}"
);
assert!(out.contains("api_key=[REDACTED]"), "got: {out}");
assert!(out.contains("api.example.com"));
assert!(
out.contains("page=2"),
"non-secret param should survive: {out}"
);
}
#[test]
fn redact_secrets_query_param_token_and_secret() {
assert!(!redact_secrets("?token=abc123def456 done").contains("abc123def456"));
assert!(!redact_secrets("client_secret=xyz789short more").contains("xyz789short"));
assert!(!redact_secrets("password = hunter2plain end").contains("hunter2plain"));
}
#[test]
fn redact_secrets_url_embedded_password() {
let text = "clone failed for https://alice:s3cr3tpw@git.example.com/repo.git";
let out = redact_secrets(text);
assert!(!out.contains("s3cr3tpw"), "URL password leaked: {out}");
assert!(out.contains("alice:[REDACTED]@"), "got: {out}");
assert!(out.contains("git.example.com"));
}
#[test]
fn redact_secrets_does_not_overredact_benign_keys() {
for benign in [
"primary_key=5",
"cache_key=user_42",
"sort_key=name",
"idempotency_key=order-123",
] {
assert_eq!(
redact_secrets(benign),
benign,
"benign key wrongly redacted: {benign}"
);
}
}
#[test]
fn redact_secrets_keyval_preserves_existing_placeholder() {
let text = "auth_token=aa83802d35bb2c4471e7e96f4eaeafa6c96fe42f set";
let out = redact_secrets(text);
assert!(!out.contains("aa83802d"), "secret leaked: {out}");
assert!(
!out.contains("[REDACTED]]"),
"placeholder got mangled: {out}"
);
}
#[test]
fn redacts_labeled_password_with_colon() {
let text = "The credentials are:\n- Email: ace@badireto.pt\n- Password: g3Jklf2!&bF6";
let out = redact_secrets(text);
assert!(!out.contains("ace@badireto.pt"), "email leaked: {out}");
assert!(!out.contains("g3Jklf2!&bF6"), "password leaked: {out}");
assert!(
out.contains("Email: [REDACTED]"),
"email not redacted: {out}"
);
assert!(
out.contains("Password: [REDACTED]"),
"password not redacted: {out}"
);
}
#[test]
fn redacts_labeled_token_with_colon() {
let text = "Token: abc123def456ghi789";
let out = redact_secrets(text);
assert!(
out.contains("Token: [REDACTED]"),
"token not redacted: {out}"
);
assert!(!out.contains("abc123def456"), "token leaked: {out}");
}
#[test]
fn redacts_labeled_api_key_with_colon() {
let text = "API Key: sk-proj-mrRb3y9swLqHv8ZzB9lPH0_V7RPruzdbnXJf34DxU2RCdQnhCYjS99Tj";
let out = redact_secrets(text);
assert!(out.contains("[REDACTED]"), "api key not redacted: {out}");
assert!(!out.contains("mrRb3y"), "api key leaked: {out}");
}
#[test]
fn redacts_email_in_sensitive_key_json() {
let input = serde_json::json!({"email": "user@example.com", "name": "John"});
let out = redact_tool_input(&input);
assert_eq!(out["email"], "[REDACTED]");
assert_eq!(out["name"], "John");
}