zerobox 0.2.2

Sandbox any command with file, network, and credential controls.
use crate::support::*;

#[test]
fn default_read_succeeds() {
    std::fs::write("/tmp/zerobox-e2e-read", "hello").expect("setup");
    let out = run(&["--", "cat", "/tmp/zerobox-e2e-read"]);
    assert!(out.status.success(), "stderr: {}", stderr(&out));
    assert_eq!(stdout(&out).trim(), "hello");
}

#[test]
fn default_write_blocked_outside_temp() {
    let home = std::env::var("HOME").expect("HOME not set");
    let target = format!("{}/zerobox-e2e-write-blocked", home);
    let out = run(&[
        "--",
        "sh",
        "-c",
        &format!("echo x > {} 2>/dev/null && echo OK || echo BLOCKED", target),
    ]);
    let _ = std::fs::remove_file(&target);
    assert!(
        stdout(&out).contains("BLOCKED"),
        "write to home should be blocked, got: {}",
        stdout(&out)
    );
}

#[test]
fn default_network_blocked() {
    let (code, ok) = curl_status(&[], "https://example.com");
    assert!(!ok, "network should be blocked, got {code}");
}

#[test]
fn allow_all_permits_everything() {
    let out = run(&[
        "--allow-all",
        "--",
        "node",
        "-e",
        "require('fs').writeFileSync('/tmp/zerobox-e2e-aa','x');console.log('ok')",
    ]);
    assert!(out.status.success(), "stderr: {}", stderr(&out));
    assert_eq!(stdout(&out).trim(), "ok");
}

#[test]
fn no_sandbox_permits_everything() {
    let out = run(&[
        "--no-sandbox",
        "--",
        "node",
        "-e",
        "require('fs').writeFileSync('/tmp/zerobox-e2e-ns','x');console.log('ok')",
    ]);
    assert!(out.status.success(), "stderr: {}", stderr(&out));
    assert_eq!(stdout(&out).trim(), "ok");
}

#[test]
fn allow_read_and_write_combined() {
    std::fs::write("/tmp/zerobox-e2e-rw-in", "input").expect("setup");
    let out = run(&[
        "--allow-read=/tmp",
        "--allow-write=/tmp",
        "--",
        "sh",
        "-c",
        "cat /tmp/zerobox-e2e-rw-in > /tmp/zerobox-e2e-rw-out && echo ok",
    ]);
    assert!(out.status.success(), "stderr: {}", stderr(&out));
    assert_eq!(stdout(&out).trim(), "ok");
    let content = std::fs::read_to_string("/tmp/zerobox-e2e-rw-out").expect("read back");
    assert_eq!(content.trim(), "input");
}

#[test]
fn allow_read_and_net_combined() {
    let (code, ok) = curl_status(
        &[
            "--allow-read=/tmp,/run",
            "--allow-write=/tmp",
            "--allow-net",
        ],
        "https://example.com",
    );
    assert!(ok, "expected 200, got {code}");
}

#[test]
fn deny_read_and_deny_write_combined() {
    let dir = setup_tmp("combo");
    let secret = dir.join("secret");
    std::fs::create_dir_all(&secret).expect("setup");
    std::fs::write(dir.join("public"), "hello").expect("setup");

    let out = run(&[
        "--profile",
        "workspace",
        &format!("--allow-write={}", dir.display()),
        &format!("--deny-write={}", secret.display()),
        "--",
        "node",
        "-e",
        &format!(
            r#"
const fs = require('fs');
let r = [];
try {{ fs.writeFileSync('{}/new.txt','x'); r.push('write-pub:ok'); }} catch(e) {{ r.push('write-pub:blocked'); }}
try {{ fs.writeFileSync('{}/secret/evil','x'); r.push('write-sec:ok'); }} catch(e) {{ r.push('write-sec:blocked'); }}
console.log(r.join(','));
"#,
            dir.display(),
            dir.display()
        ),
    ]);
    assert!(out.status.success(), "stderr: {}", stderr(&out));
    let result = stdout(&out).trim().to_string();
    assert!(result.contains("write-pub:ok"), "got: {result}");
    assert!(result.contains("write-sec:blocked"), "got: {result}");
}

#[test]
fn allow_net_domain_with_write_restriction() {
    let dir = setup_tmp("net-write");
    let (code, ok) = curl_status(
        &[
            "--allow-net=example.com",
            &format!("--allow-write={}", dir.display()),
        ],
        "https://example.com",
    );
    assert!(ok, "expected 200, got {code}");
}

#[test]
fn exit_code_zero_propagated() {
    let out = run(&[
        "--profile",
        "workspace",
        "--",
        "node",
        "-e",
        "process.exit(0)",
    ]);
    assert!(out.status.success());
}

#[test]
fn exit_code_nonzero_propagated() {
    let out = run(&[
        "--profile",
        "workspace",
        "--",
        "node",
        "-e",
        "process.exit(42)",
    ]);
    assert_eq!(out.status.code(), Some(42));
}

#[test]
fn relative_write_path_resolved() {
    let out = run(&[
        "--profile",
        "workspace",
        "-C",
        "/tmp",
        "--",
        "node",
        "-e",
        "require('fs').writeFileSync('/tmp/zerobox-e2e-rel','ok');console.log('ok')",
    ]);
    assert!(out.status.success(), "stderr: {}", stderr(&out));
}

#[test]
fn default_profile_blocks_home_read() {
    let home = std::env::var("HOME").expect("HOME not set");
    let target = format!("{}/zerobox-e2e-read-test", home);
    std::fs::write(&target, "secret").expect("setup");
    let out = run(&["--", "cat", &target]);
    let _ = std::fs::remove_file(&target);
    assert!(
        !out.status.success(),
        "home files should not be readable with default profile"
    );
}

#[test]
fn default_profile_blocks_home_write() {
    let home = std::env::var("HOME").expect("HOME not set");
    let target = format!("{}/zerobox-e2e-write-test", home);
    let out = run(&[
        "--",
        "sh",
        "-c",
        &format!(
            "echo x > {} 2>/dev/null && echo WRITTEN || echo BLOCKED",
            target
        ),
    ]);
    assert!(
        !stdout(&out).contains("WRITTEN"),
        "writes to home should be blocked, got: {}",
        stdout(&out)
    );
    let _ = std::fs::remove_file(&target);
}

#[test]
fn workspace_profile_provides_cwd_read_write() {
    let dir = setup_tmp("ws-cwd");
    std::fs::write(dir.join("input.txt"), "hello").expect("setup");
    let out = run(&[
        "--profile",
        "workspace",
        "-C",
        &dir.display().to_string(),
        "--",
        "sh",
        "-c",
        "cat input.txt && echo world > output.txt && echo ok",
    ]);
    assert!(out.status.success(), "stderr: {}", stderr(&out));
    assert!(stdout(&out).contains("hello"));
    assert!(stdout(&out).contains("ok"));
    assert_eq!(
        std::fs::read_to_string(dir.join("output.txt"))
            .unwrap()
            .trim(),
        "world"
    );
}

#[test]
fn invalid_profile_name_rejected() {
    let out = run(&["--profile", "../../../etc/passwd", "--", "echo", "hello"]);
    assert!(!out.status.success());
    assert!(
        stderr(&out).contains("invalid profile name"),
        "stderr: {}",
        stderr(&out)
    );
}

#[test]
fn nonexistent_command_fails() {
    let out = run(&["--", "this-command-does-not-exist-zerobox"]);
    assert!(!out.status.success());
}

mod strict_flag {
    use super::*;

    #[test]
    fn strict_works_when_namespaces_available() {
        let out = run(&["--strict-sandbox", "--", "echo", "hello"]);
        assert!(
            out.status.success(),
            "strict should work when namespaces are available, stderr: {}",
            stderr(&out)
        );
        assert_eq!(stdout(&out).trim(), "hello");
    }

    #[test]
    fn strict_with_allow_write() {
        let out = run(&[
            "--strict-sandbox",
            "--allow-write=/tmp",
            "--",
            "sh",
            "-c",
            "echo ok > /tmp/zerobox-strict-test && cat /tmp/zerobox-strict-test",
        ]);
        assert!(out.status.success(), "stderr: {}", stderr(&out));
        assert_eq!(stdout(&out).trim(), "ok");
    }
}