santh-error 0.2.0

Actionable error primitives - stable error codes, fix hints, and built-in secret redaction
Documentation
use santh_error::{redact_secrets, SanthError};

#[test]
fn redact_embedded_jwt() {
    let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
    let input = format!("token={}", jwt);
    let out = redact_secrets(&input);
    assert!(!out.contains("eyJhbGci"));
    assert!(out.contains("[REDACTED]"));
}

#[test]
fn redact_github_token() {
    let input = "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    let out = redact_secrets(input);
    assert!(!out.contains("ghp_"));
    assert!(out.contains("[REDACTED]"));
}

#[test]
fn redact_github_fine_grained_pat() {
    let input = "github_pat_11ABCDEIJ0lmNoPQRstu_vwxyz12345678901234567890ABCDEFGHIJK";
    let out = redact_secrets(input);
    assert!(!out.contains("github_pat_"));
    assert!(out.contains("[REDACTED]"));
}

#[test]
fn redact_password_field() {
    let cases = [
        ("password=hunter2", "hunter2"),
        ("passwd:hunter2", "hunter2"),
        ("Password = hunter2", "hunter2"),
        ("PASSWORD:hunter2", "hunter2"),
        ("api_key=secret123", "secret123"),
        ("api-key:secret123", "secret123"),
        ("token=abcd", "abcd"),
        ("secret=shh", "shh"),
    ];

    for (input, secret) in &cases {
        let out = redact_secrets(input);
        assert!(!out.contains(secret), "failed to redact secret in: {input}");
        assert!(out.contains("[REDACTED]"), "missing [REDACTED] in: {input}");
    }
}

#[test]
fn redact_bearer_token() {
    let input = "Authorization: Bearer abc123def456";
    let out = redact_secrets(input);
    assert!(!out.contains("abc123def456"));
    assert!(out.contains("[REDACTED]"));
}

#[test]
fn redact_openai_key() {
    let input = "sk-abcdefghijklmnopqrstuvwxyz123456";
    let out = redact_secrets(input);
    assert!(!out.contains("sk-abcdefghijklmnopqrstuvwxyz123456"));
    assert!(out.contains("[REDACTED]"));
}

#[test]
fn redact_private_key() {
    let key = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...\n-----END PRIVATE KEY-----";
    let out = redact_secrets(key);
    assert!(!out.contains("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC"));
    assert!(out.contains("[REDACTED]"));
}

#[test]
fn redact_rsa_private_key() {
    let key = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----";
    let out = redact_secrets(key);
    assert!(!out.contains("MIIEpAIBAAKCAQEA"));
    assert!(out.contains("[REDACTED]"));
}

#[test]
fn unicode_in_context_values() {
    let err = SanthError::new("TEST-U001", "Unicode test")
        .with_context("emoji", "🚀🔥💀")
        .with_context("chinese", "这是一个测试")
        .with_context("arabic", "هذا اختبار")
        .with_context("zalgo", "T̷͓̖͈̲̩̗h̴͍͙͚͕͓i̶͈s̷̡̛̞")
        .fix("Fix: No action needed, this is a test.")
        .build();

    let msg = err.actionable_message();
    assert!(msg.contains("🚀🔥💀"));
    assert!(msg.contains("这是一个测试"));
    assert!(msg.contains("هذا اختبار"));
    assert!(msg.contains("T̷͓̖͈̲̩̗h̴͍͙͚͕͓i̶͈s̷̡̛̞"));
    assert!(msg.contains("Fix: "));
}

#[test]
fn very_long_message_redaction() {
    let secret = "sk-".to_string() + &"a".repeat(100_000);
    let input = format!("prefix {} suffix", secret);
    let out = redact_secrets(&input);
    assert!(!out.contains(&secret));
    assert!(out.contains("[REDACTED]"));
    assert!(out.starts_with("prefix "));
    assert!(out.ends_with(" suffix"));
}

#[test]
fn multiple_secrets_in_one_string() {
    let input = "password=foo api_key=bar token=baz secret=qux";
    let out = redact_secrets(input);
    assert!(!out.contains("foo"));
    assert!(!out.contains("bar"));
    assert!(!out.contains("baz"));
    assert!(!out.contains("qux"));
}

#[test]
fn secret_in_context_gets_redacted_in_actionable_message() {
    let err = SanthError::new("TEST-SEC-01", "Request failed")
        .with_context("auth", "Bearer supersecrettoken12345")
        .fix("Fix: Retry the request with a valid token.")
        .build();

    let msg = err.actionable_message();
    assert!(!msg.contains("supersecrettoken12345"));
    assert!(msg.contains("[REDACTED]"));
}

#[test]
fn aws_key_in_title_gets_redacted() {
    let err = SanthError::new("TEST-SEC-02", "Key AKIAIOSFODNN7EXAMPLE is invalid")
        .fix("Fix: Rotate the AWS access key.")
        .build();

    let msg = err.actionable_message();
    assert!(!msg.contains("AKIAIOSFODNN7EXAMPLE"));
    assert!(msg.contains("[REDACTED]"));
}