zerobox 0.3.3

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

#[test]
fn default_env_has_path() {
    let out = run(&["--allow-all", "--", "sh", "-c", "echo $PATH"]);
    assert!(out.status.success());
    assert!(!stdout(&out).trim().is_empty(), "PATH should be set");
}

#[test]
fn default_env_has_home() {
    let out = run(&["--allow-all", "--", "sh", "-c", "echo $HOME"]);
    assert!(out.status.success());
    assert!(!stdout(&out).trim().is_empty(), "HOME should be set");
}

#[test]
fn default_env_excludes_non_essential_vars() {
    let out = Command::new(zerobox_exec())
        .args(["--allow-all", "--", "sh", "-c", "echo $ZEROBOX_TEST_VAR"])
        .env("ZEROBOX_TEST_VAR", "should_not_appear")
        .output()
        .expect("spawn");
    assert!(out.status.success());
    assert_eq!(
        stdout(&out).trim(),
        "",
        "custom var should not be inherited"
    );
}

#[test]
fn default_env_is_minimal() {
    let out = run(&["--allow-all", "--", "env"]);
    assert!(out.status.success());
    let count = stdout(&out).lines().count();
    assert!(count <= 10, "expected minimal env, got {count} vars");
}

#[test]
fn env_sets_explicit_var() {
    let out = run(&[
        "--allow-all",
        "--env",
        "FOO=bar",
        "--",
        "sh",
        "-c",
        "echo $FOO",
    ]);
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "bar");
}

#[test]
fn env_multiple_vars() {
    let out = run(&[
        "--allow-all",
        "--env",
        "A=1",
        "--env",
        "B=2",
        "--",
        "sh",
        "-c",
        "echo $A $B",
    ]);
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "1 2");
}

#[test]
fn env_value_with_equals() {
    let out = run(&[
        "--allow-all",
        "--env",
        "DATA=key=value=extra",
        "--",
        "sh",
        "-c",
        "echo $DATA",
    ]);
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "key=value=extra");
}

#[test]
fn env_empty_value() {
    let out = run(&[
        "--allow-all",
        "--env",
        "EMPTY=",
        "--",
        "sh",
        "-c",
        "echo \"x${EMPTY}x\"",
    ]);
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "xx");
}

#[test]
fn env_overrides_inherited() {
    let out = run(&[
        "--allow-all",
        "--allow-env",
        "--env",
        "HOME=/custom",
        "--",
        "sh",
        "-c",
        "echo $HOME",
    ]);
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "/custom");
}

#[test]
fn allow_env_bare_inherits_all() {
    let out = Command::new(zerobox_exec())
        .args([
            "--allow-all",
            "--allow-env",
            "--",
            "sh",
            "-c",
            "echo $ZEROBOX_TEST_ALL",
        ])
        .env("ZEROBOX_TEST_ALL", "visible")
        .output()
        .expect("spawn");
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "visible");
}

#[test]
fn allow_env_bare_has_many_vars() {
    let out = run(&["--allow-all", "--allow-env", "--", "env"]);
    assert!(out.status.success());
    let count = stdout(&out).lines().count();
    assert!(
        count > 10,
        "expected many vars with --allow-env, got {count}"
    );
}

#[test]
fn allow_env_specific_keys() {
    let out = run(&["--allow-all", "--allow-env=PATH", "--", "env"]);
    assert!(out.status.success());
    let lines = stdout(&out);
    assert!(lines.contains("PATH="), "PATH should be present");
    assert!(!lines.contains("HOME="), "HOME should not be present");
}

#[test]
fn allow_env_specific_with_env_override() {
    let out = run(&[
        "--allow-all",
        "--allow-env=PATH",
        "--env",
        "FOO=added",
        "--",
        "sh",
        "-c",
        "echo FOO=$FOO HOME=$HOME",
    ]);
    assert!(out.status.success());
    let s = stdout(&out).trim().to_string();
    assert!(
        s.contains("FOO=added"),
        "explicit --env should survive, got: {s}"
    );
    assert!(s.contains("HOME="), "HOME should be empty");
    assert!(!s.contains("HOME=/"), "HOME should not have a value");
}

#[test]
fn deny_env_removes_var() {
    let out = run(&[
        "--allow-all",
        "--allow-env",
        "--deny-env=HOME",
        "--",
        "sh",
        "-c",
        "echo \"HOME=$HOME\"",
    ]);
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "HOME=");
}

#[test]
fn deny_env_takes_precedence_over_allow() {
    let out = run(&[
        "--allow-all",
        "--allow-env=PATH,HOME",
        "--deny-env=HOME",
        "--",
        "env",
    ]);
    assert!(out.status.success());
    let lines = stdout(&out);
    assert!(lines.contains("PATH="), "PATH should survive");
    assert!(!lines.contains("HOME="), "HOME should be denied");
}

#[test]
fn deny_env_does_not_block_explicit_env() {
    let out = run(&[
        "--allow-all",
        "--allow-env",
        "--deny-env=FOO",
        "--env",
        "FOO=override",
        "--",
        "sh",
        "-c",
        "echo $FOO",
    ]);
    assert!(out.status.success());
    assert_eq!(stdout(&out).trim(), "override");
}

#[test]
fn deny_env_multiple_keys() {
    let out = run(&[
        "--allow-all",
        "--allow-env",
        "--deny-env=HOME,USER",
        "--",
        "sh",
        "-c",
        "echo \"HOME=$HOME USER=$USER\"",
    ]);
    assert!(out.status.success());
    let s = stdout(&out).trim().to_string();
    assert_eq!(s, "HOME= USER=");
}

#[test]
fn env_without_equals_fails() {
    let out = run(&["--allow-all", "--env", "BADFORMAT", "--", "echo", "hi"]);
    assert!(!out.status.success());
    assert!(
        stderr(&out).contains("KEY=VALUE"),
        "should hint at format, got: {}",
        stderr(&out)
    );
}