kelora 0.7.0

A command-line log analysis tool with embedded Rhai scripting
use std::process::Command;

#[test]
fn test_bucket_function() {
    // Feed input via stdin
    let output = Command::new("./target/release/kelora")
        .args(["-f", "line", "--exec", "e.bucket = bucket(e.line)"])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"test\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("bucket="));
}

#[test]
fn test_hash_function_default() {
    let output = Command::new("./target/release/kelora")
        .args(["-f", "line", "--exec", "e.hash = hash(e.line)"])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"hello\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stdout = String::from_utf8_lossy(&output.stdout);
    // SHA-256 hash of "hello"
    assert!(stdout.contains("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"));
}

#[test]
fn test_hash_function_with_algorithm() {
    let output = Command::new("./target/release/kelora")
        .args([
            "-f",
            "line",
            "--exec",
            r#"e.hash_md5 = hash(e.line, "md5")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"hello\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stdout = String::from_utf8_lossy(&output.stdout);
    // MD5 hash of "hello"
    assert!(stdout.contains("5d41402abc4b2a76b9719d911017c592"));
}

#[test]
fn test_pseudonym_with_env_secret() {
    let output = Command::new("./target/release/kelora")
        .env("KELORA_SECRET", "test_secret_12345")
        .args([
            "-vv",
            "-f",
            "line",
            "--exec",
            r#"e.pseudo = pseudonym(e.line, "kelora:v1:user")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"user123\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    // Should log stable mode (only visible with -vv)
    assert!(stderr.contains("pseudonym: ON (stable; KELORA_SECRET)"));
    // Should produce pseudonym
    assert!(stdout.contains("pseudo="));
}

#[test]
fn test_pseudonym_ephemeral_mode() {
    let output = Command::new("./target/release/kelora")
        .env_remove("KELORA_SECRET")
        .args([
            "-vv",
            "-f",
            "line",
            "--exec",
            r#"e.pseudo = pseudonym(e.line, "kelora:v1:user")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"user123\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    // Should log ephemeral mode (only visible with -vv)
    assert!(stderr.contains("pseudonym: ON (ephemeral; not stable)"));
    // Should produce pseudonym
    assert!(stdout.contains("pseudo="));
}

#[test]
fn test_pseudonym_domain_separation() {
    let output = Command::new("./target/release/kelora")
        .env("KELORA_SECRET", "test_secret_12345")
        .args([
            "-f",
            "line",
            "--exec",
            r#"e.pseudo_email = pseudonym(e.line, "kelora:v1:email"); e.pseudo_ip = pseudonym(e.line, "kelora:v1:ip")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"user123\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stdout = String::from_utf8_lossy(&output.stdout);

    // Both pseudonyms should be present
    assert!(stdout.contains("pseudo_email="));
    assert!(stdout.contains("pseudo_ip="));

    // Extract the values to check they're different (not easily done in shell)
    // For now, just verify both are present
}

#[test]
fn test_pseudonym_deterministic() {
    // Run twice with same secret
    let output1 = Command::new("./target/release/kelora")
        .env("KELORA_SECRET", "test_secret_12345")
        .args([
            "-f",
            "line",
            "--exec",
            r#"e.pseudo = pseudonym(e.line, "kelora:v1:user")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"user123\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let output2 = Command::new("./target/release/kelora")
        .env("KELORA_SECRET", "test_secret_12345")
        .args([
            "-f",
            "line",
            "--exec",
            r#"e.pseudo = pseudonym(e.line, "kelora:v1:user")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"user123\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stdout1 = String::from_utf8_lossy(&output1.stdout);
    let stdout2 = String::from_utf8_lossy(&output2.stdout);

    // Should produce identical output (deterministic)
    assert_eq!(stdout1, stdout2);
}

#[test]
fn test_pseudonym_empty_domain_error() {
    let output = Command::new("./target/release/kelora")
        .env("KELORA_SECRET", "test_secret_12345")
        .args([
            "-f",
            "line",
            "--exec",
            r#"e.pseudo = pseudonym(e.line, "")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"user123\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("domain must be non-empty"));
}

#[test]
fn test_pseudonym_empty_secret_error() {
    let output = Command::new("./target/release/kelora")
        .env("KELORA_SECRET", "")
        .args([
            "-f",
            "line",
            "--exec",
            r#"e.pseudo = pseudonym(e.line, "kelora:v1:user")"#,
        ])
        .arg("-")
        .stdin(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(stdin) = child.stdin.as_mut() {
                stdin.write_all(b"user123\n").ok();
            }
            child.wait_with_output()
        })
        .expect("Failed to execute kelora");

    let stderr = String::from_utf8_lossy(&output.stderr);
    // Should fail with message about empty secret
    assert!(stderr.contains("KELORA_SECRET must not be empty"));
    // Process should exit with error
    assert!(!output.status.success());
}