700 lines
19 KiB
Go
700 lines
19 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestMain creates a fake landrun binary so IsAvailable() returns true for all tests.
|
|
func TestMain(m *testing.M) {
|
|
tmpDir, err := os.MkdirTemp("", "sandbox-test-*")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fakeLandrun := filepath.Join(tmpDir, "landrun")
|
|
if err := os.WriteFile(fakeLandrun, []byte("#!/bin/sh\necho 'landrun 0.0.0'\n"), 0755); err != nil {
|
|
panic(err)
|
|
}
|
|
os.Setenv("PATH", tmpDir+":"+os.Getenv("PATH"))
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// ---- expandPath ----
|
|
|
|
func TestExpandPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pattern string
|
|
cwd string
|
|
env map[string]string
|
|
want string
|
|
}{
|
|
{
|
|
name: "literal",
|
|
pattern: "/etc/passwd",
|
|
cwd: "/cwd",
|
|
want: "/etc/passwd",
|
|
},
|
|
{
|
|
name: "CWD",
|
|
pattern: "{CWD}/foo",
|
|
cwd: "/my/cwd",
|
|
want: "/my/cwd/foo",
|
|
},
|
|
{
|
|
name: "TMPDIR from env",
|
|
pattern: "{TMPDIR}",
|
|
cwd: "/cwd",
|
|
env: map[string]string{"TMPDIR": "/custom/tmp"},
|
|
want: "/custom/tmp",
|
|
},
|
|
{
|
|
name: "TMPDIR default",
|
|
pattern: "{TMPDIR}",
|
|
cwd: "/cwd",
|
|
env: map[string]string{"TMPDIR": ""},
|
|
want: "/tmp",
|
|
},
|
|
{
|
|
name: "custom env var",
|
|
pattern: "{MY_SANDBOX_TEST_VAR}/bar",
|
|
cwd: "/cwd",
|
|
env: map[string]string{"MY_SANDBOX_TEST_VAR": "/custom"},
|
|
want: "/custom/bar",
|
|
},
|
|
{
|
|
name: "unknown var unchanged",
|
|
pattern: "{NONEXISTENT_SANDBOX_VAR_XYZ}",
|
|
cwd: "/cwd",
|
|
want: "{NONEXISTENT_SANDBOX_VAR_XYZ}",
|
|
},
|
|
{
|
|
name: "XDG_CONFIG_HOME from env",
|
|
pattern: "{XDG_CONFIG_HOME}/app",
|
|
cwd: "/cwd",
|
|
env: map[string]string{"XDG_CONFIG_HOME": "/xdg/config"},
|
|
want: "/xdg/config/app",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for k, v := range tc.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
got := expandPath(tc.pattern, tc.cwd, os.Getenv)
|
|
if got != tc.want {
|
|
t.Errorf("expandPath(%q, %q) = %q, want %q", tc.pattern, tc.cwd, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---- WrapCommand ----
|
|
|
|
func TestWrapCommand(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
rwDir := filepath.Join(tmpDir, "rw")
|
|
roDir := filepath.Join(tmpDir, "ro")
|
|
if err := os.MkdirAll(rwDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(roDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
t.Run("starts with landrun", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"echo", "hi"}, tmpDir)
|
|
if len(got) == 0 || got[0] != "landrun" {
|
|
t.Errorf("expected landrun as first arg, got %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("separator before original command", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"echo", "test"}, tmpDir)
|
|
sepIdx := indexOf(got, "--")
|
|
if sepIdx == -1 {
|
|
t.Fatal("missing -- separator")
|
|
}
|
|
if got[sepIdx+1] != "echo" || got[sepIdx+2] != "test" {
|
|
t.Errorf("expected [echo test] after --, got %v", got[sepIdx+1:])
|
|
}
|
|
})
|
|
|
|
t.Run("log level flag", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{LogLevel: "debug"}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--log-level", "debug")
|
|
})
|
|
|
|
t.Run("no log level when empty", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{LogLevel: ""}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertDoesNotContain(t, got, "--log-level")
|
|
})
|
|
|
|
t.Run("best effort flag", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{BestEffort: true}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertContains(t, got, "--best-effort")
|
|
})
|
|
|
|
t.Run("no best effort when false", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{BestEffort: false}, Advanced: AdvancedConfig{}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertDoesNotContain(t, got, "--best-effort")
|
|
})
|
|
|
|
t.Run("ldd flag", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{LDD: true}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertContains(t, got, "--ldd")
|
|
})
|
|
|
|
t.Run("add-exec flag", func(t *testing.T) {
|
|
cfg := &Config{General: GeneralConfig{}, Advanced: AdvancedConfig{AddExec: true}, Network: NetworkConfig{}}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertContains(t, got, "--add-exec")
|
|
})
|
|
|
|
t.Run("rw path included when exists", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Filesystem: FilesystemConfig{RW: []string{rwDir}},
|
|
Network: NetworkConfig{},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--rw", rwDir)
|
|
})
|
|
|
|
t.Run("ro path included when exists", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Filesystem: FilesystemConfig{RO: []string{roDir}},
|
|
Network: NetworkConfig{},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--ro", roDir)
|
|
})
|
|
|
|
t.Run("nonexistent path excluded", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Filesystem: FilesystemConfig{RW: []string{"/nonexistent/path/that/does/not/exist/xyz"}},
|
|
Network: NetworkConfig{},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertDoesNotContain(t, got, "--rw")
|
|
})
|
|
|
|
t.Run("CWD template expanded to real path", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Filesystem: FilesystemConfig{RW: []string{"{CWD}"}},
|
|
Network: NetworkConfig{},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--rw", tmpDir)
|
|
})
|
|
|
|
t.Run("unrestricted network", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Network: NetworkConfig{Enabled: true, Unrestricted: true},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertContains(t, got, "--unrestricted-network")
|
|
assertDoesNotContain(t, got, "--connect-tcp")
|
|
assertDoesNotContain(t, got, "--bind-tcp")
|
|
})
|
|
|
|
t.Run("restricted network ports", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Network: NetworkConfig{
|
|
Enabled: true,
|
|
ConnectTCP: []string{"443", "80"},
|
|
BindTCP: []string{"8080"},
|
|
},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--connect-tcp", "443")
|
|
assertFlagValue(t, got, "--connect-tcp", "80")
|
|
assertFlagValue(t, got, "--bind-tcp", "8080")
|
|
assertDoesNotContain(t, got, "--unrestricted-network")
|
|
})
|
|
|
|
t.Run("network disabled ignores ports", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Network: NetworkConfig{Enabled: false, ConnectTCP: []string{"443"}},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertDoesNotContain(t, got, "--connect-tcp")
|
|
assertDoesNotContain(t, got, "--unrestricted-network")
|
|
})
|
|
|
|
t.Run("env vars", func(t *testing.T) {
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Network: NetworkConfig{},
|
|
Env: []string{"HOME", "PATH"},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--env", "HOME")
|
|
assertFlagValue(t, got, "--env", "PATH")
|
|
})
|
|
|
|
t.Run("parent path sorted before child", func(t *testing.T) {
|
|
parent := filepath.Join(tmpDir, "parent")
|
|
child := filepath.Join(tmpDir, "parent", "child")
|
|
if err := os.MkdirAll(child, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := &Config{
|
|
General: GeneralConfig{},
|
|
Advanced: AdvancedConfig{},
|
|
Filesystem: FilesystemConfig{RW: []string{child, parent}}, // child listed first
|
|
Network: NetworkConfig{},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
parentIdx := indexOf(got, parent)
|
|
childIdx := indexOf(got, child)
|
|
if parentIdx == -1 || childIdx == -1 {
|
|
t.Fatal("parent or child path missing from args")
|
|
}
|
|
if parentIdx > childIdx {
|
|
t.Errorf("parent (idx %d) should appear before child (idx %d) in args", parentIdx, childIdx)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- checkPath ----
|
|
|
|
func TestCheckPath(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
rwDir := filepath.Join(tmpDir, "rw")
|
|
roDir := filepath.Join(tmpDir, "ro")
|
|
if err := os.MkdirAll(rwDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(roDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
Filesystem: FilesystemConfig{
|
|
RO: []string{roDir},
|
|
RW: []string{rwDir},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
write bool
|
|
wantErr bool
|
|
}{
|
|
{"rw path read allowed", filepath.Join(rwDir, "file.txt"), false, false},
|
|
{"rw path write allowed", filepath.Join(rwDir, "file.txt"), true, false},
|
|
{"ro path read allowed", filepath.Join(roDir, "file.txt"), false, false},
|
|
{"ro path write denied", filepath.Join(roDir, "file.txt"), true, true},
|
|
{"outside path read denied", "/outside/path/xyz", false, true},
|
|
{"outside path write denied", "/outside/path/xyz", true, true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := checkPath(cfg, tc.path, tc.write, tmpDir, os.Getenv)
|
|
if (err != nil) != tc.wantErr {
|
|
t.Errorf("checkPath(%q, write=%v) error = %v, wantErr %v", tc.path, tc.write, err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("CWD template resolved", func(t *testing.T) {
|
|
cfg2 := &Config{Filesystem: FilesystemConfig{RW: []string{"{CWD}"}}}
|
|
err := checkPath(cfg2, filepath.Join(tmpDir, "newfile.txt"), true, tmpDir, os.Getenv)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("RWX grants write access", func(t *testing.T) {
|
|
rwxDir := filepath.Join(tmpDir, "rwx")
|
|
if err := os.MkdirAll(rwxDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg3 := &Config{Filesystem: FilesystemConfig{RWX: []string{rwxDir}}}
|
|
if err := checkPath(cfg3, filepath.Join(rwxDir, "bin"), true, tmpDir, os.Getenv); err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- LoadSandbox ----
|
|
|
|
func TestLoadSandbox(t *testing.T) {
|
|
t.Run("full config", func(t *testing.T) {
|
|
yaml := []byte(`
|
|
general:
|
|
best_effort: true
|
|
log_level: debug
|
|
filesystem:
|
|
ro:
|
|
- /etc/passwd
|
|
rox:
|
|
- /usr
|
|
rw:
|
|
- "{CWD}"
|
|
rwx:
|
|
- "{XDG_CONFIG_HOME}/ollie"
|
|
network:
|
|
enabled: true
|
|
unrestricted: false
|
|
bind_tcp:
|
|
- "8080"
|
|
connect_tcp:
|
|
- "443"
|
|
env:
|
|
- HOME
|
|
- PATH
|
|
advanced:
|
|
ldd: true
|
|
add_exec: false
|
|
`)
|
|
cfg, err := LoadSandbox(bytes.NewReader(yaml))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !cfg.General.BestEffort {
|
|
t.Error("BestEffort should be true")
|
|
}
|
|
if cfg.General.LogLevel != "debug" {
|
|
t.Errorf("LogLevel = %q, want %q", cfg.General.LogLevel, "debug")
|
|
}
|
|
if !containsStr(cfg.Filesystem.RO, "/etc/passwd") {
|
|
t.Error("RO missing /etc/passwd")
|
|
}
|
|
if !containsStr(cfg.Filesystem.ROX, "/usr") {
|
|
t.Error("ROX missing /usr")
|
|
}
|
|
if !containsStr(cfg.Filesystem.RW, "{CWD}") {
|
|
t.Error("RW missing {CWD}")
|
|
}
|
|
if !containsStr(cfg.Filesystem.RWX, "{XDG_CONFIG_HOME}/ollie") {
|
|
t.Error("RWX missing {XDG_CONFIG_HOME}/ollie")
|
|
}
|
|
if !cfg.Network.Enabled {
|
|
t.Error("Network.Enabled should be true")
|
|
}
|
|
if cfg.Network.Unrestricted {
|
|
t.Error("Network.Unrestricted should be false")
|
|
}
|
|
if !containsStr(cfg.Network.BindTCP, "8080") {
|
|
t.Error("BindTCP missing 8080")
|
|
}
|
|
if !containsStr(cfg.Network.ConnectTCP, "443") {
|
|
t.Error("ConnectTCP missing 443")
|
|
}
|
|
if !containsStr(cfg.Env, "HOME") || !containsStr(cfg.Env, "PATH") {
|
|
t.Error("Env missing HOME or PATH")
|
|
}
|
|
if !cfg.Advanced.LDD {
|
|
t.Error("Advanced.LDD should be true")
|
|
}
|
|
if cfg.Advanced.AddExec {
|
|
t.Error("Advanced.AddExec should be false")
|
|
}
|
|
})
|
|
|
|
t.Run("empty yaml produces zero config", func(t *testing.T) {
|
|
cfg, err := LoadSandbox(bytes.NewReader(nil))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cfg.General.BestEffort || cfg.General.LogLevel != "" {
|
|
t.Error("expected zero GeneralConfig")
|
|
}
|
|
if len(cfg.Filesystem.RW) != 0 || len(cfg.Env) != 0 {
|
|
t.Error("expected empty filesystem and env")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid yaml returns error", func(t *testing.T) {
|
|
_, err := LoadSandbox(bytes.NewReader([]byte("{ not: valid: yaml: [")))
|
|
if err == nil {
|
|
t.Error("expected error for invalid yaml")
|
|
}
|
|
})
|
|
|
|
t.Run("partial config leaves other fields zero", func(t *testing.T) {
|
|
yaml := []byte(`
|
|
env:
|
|
- TERM
|
|
`)
|
|
cfg, err := LoadSandbox(bytes.NewReader(yaml))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !containsStr(cfg.Env, "TERM") {
|
|
t.Error("Env missing TERM")
|
|
}
|
|
if len(cfg.Filesystem.RW) != 0 {
|
|
t.Error("expected empty RW")
|
|
}
|
|
})
|
|
}
|
|
|
|
// ---- expandPath: remaining branches ----
|
|
|
|
func TestExpandPath_HOME(t *testing.T) {
|
|
got := expandPath("{HOME}/test", "/cwd", os.Getenv)
|
|
home, _ := os.UserHomeDir()
|
|
if got != home+"/test" {
|
|
t.Errorf("expandPath({HOME}/test) = %q; want %q", got, home+"/test")
|
|
}
|
|
}
|
|
|
|
func TestExpandPath_XDG_Defaults(t *testing.T) {
|
|
home, _ := os.UserHomeDir()
|
|
for _, tc := range []struct {
|
|
varName, pattern, want string
|
|
}{
|
|
{"XDG_CONFIG_HOME", "{XDG_CONFIG_HOME}", filepath.Join(home, ".config")},
|
|
{"XDG_DATA_HOME", "{XDG_DATA_HOME}", filepath.Join(home, ".local/share")},
|
|
{"XDG_CACHE_HOME", "{XDG_CACHE_HOME}", filepath.Join(home, ".cache")},
|
|
{"XDG_STATE_HOME", "{XDG_STATE_HOME}", filepath.Join(home, ".local/state")},
|
|
{"XDG_RUNTIME_DIR", "{XDG_RUNTIME_DIR}", fmt.Sprintf("/run/user/%d", os.Getuid())},
|
|
} {
|
|
t.Run(tc.varName+"_default", func(t *testing.T) {
|
|
t.Setenv(tc.varName, "")
|
|
got := expandPath(tc.pattern, "/cwd", os.Getenv)
|
|
if got != tc.want {
|
|
t.Errorf("expandPath(%q) = %q; want %q", tc.pattern, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpandPath_XDG_FromEnv(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
varName, pattern string
|
|
}{
|
|
{"XDG_DATA_HOME", "{XDG_DATA_HOME}"},
|
|
{"XDG_CACHE_HOME", "{XDG_CACHE_HOME}"},
|
|
{"XDG_STATE_HOME", "{XDG_STATE_HOME}"},
|
|
{"XDG_RUNTIME_DIR", "{XDG_RUNTIME_DIR}"},
|
|
} {
|
|
t.Run(tc.varName+"_env", func(t *testing.T) {
|
|
t.Setenv(tc.varName, "/custom/"+tc.varName)
|
|
got := expandPath(tc.pattern, "/cwd", os.Getenv)
|
|
if got != "/custom/"+tc.varName {
|
|
t.Errorf("expandPath(%q) = %q; want /custom/%s", tc.pattern, got, tc.varName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpandPath_XDGPaths(t *testing.T) {
|
|
// XDG_CONFIG_HOME expansion
|
|
t.Setenv("XDG_CONFIG_HOME", "/custom/config")
|
|
got1 := expandPath("{XDG_CONFIG_HOME}/ollie", "/cwd", os.Getenv)
|
|
if got1 != "/custom/config/ollie" {
|
|
t.Errorf("XDG_CONFIG_HOME/ollie not expanded: %q", got1)
|
|
}
|
|
// XDG_DATA_HOME expansion
|
|
t.Setenv("XDG_DATA_HOME", "/custom/data")
|
|
got2 := expandPath("{XDG_DATA_HOME}/ollie", "/cwd", os.Getenv)
|
|
if got2 != "/custom/data/ollie" {
|
|
t.Errorf("XDG_DATA_HOME/ollie not expanded: %q", got2)
|
|
}
|
|
}
|
|
|
|
// ---- checkPath: remaining branches ----
|
|
|
|
func TestCheckPath_SymlinkResolved(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
realDir := filepath.Join(tmpDir, "real")
|
|
os.MkdirAll(realDir, 0755)
|
|
// Create a real file so EvalSymlinks succeeds on the full path
|
|
realFile := filepath.Join(realDir, "file")
|
|
os.WriteFile(realFile, []byte("x"), 0644)
|
|
link := filepath.Join(tmpDir, "link")
|
|
os.Symlink(realDir, link)
|
|
|
|
cfg := &Config{Filesystem: FilesystemConfig{RW: []string{realDir}}}
|
|
// Access via symlink — EvalSymlinks resolves link/file to real/file
|
|
if err := checkPath(cfg, filepath.Join(link, "file"), true, tmpDir, os.Getenv); err != nil {
|
|
t.Errorf("symlink path should be allowed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckPath_ROX_ReadAllowed(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
roxDir := filepath.Join(tmpDir, "rox")
|
|
os.MkdirAll(roxDir, 0755)
|
|
|
|
cfg := &Config{Filesystem: FilesystemConfig{ROX: []string{roxDir}}}
|
|
if err := checkPath(cfg, filepath.Join(roxDir, "bin"), false, tmpDir, os.Getenv); err != nil {
|
|
t.Errorf("ROX read should be allowed: %v", err)
|
|
}
|
|
if err := checkPath(cfg, filepath.Join(roxDir, "bin"), true, tmpDir, os.Getenv); err == nil {
|
|
t.Error("ROX write should be denied")
|
|
}
|
|
}
|
|
|
|
// ---- LoadSandbox: reader error ----
|
|
|
|
func TestLoadSandbox_ReaderError(t *testing.T) {
|
|
_, err := LoadSandbox(&errReader{})
|
|
if err == nil {
|
|
t.Error("expected error from bad reader")
|
|
}
|
|
}
|
|
|
|
type errReader struct{}
|
|
|
|
func (errReader) Read([]byte) (int, error) { return 0, fmt.Errorf("read error") }
|
|
|
|
// ---- WrapCommand: remaining branches ----
|
|
|
|
func TestWrapCommand_LandrunUnavailable(t *testing.T) {
|
|
old := isAvailableFn
|
|
isAvailableFn = func() bool { return false }
|
|
defer func() { isAvailableFn = old }()
|
|
|
|
cfg := &Config{}
|
|
_, err := WrapCommand(cfg, []string{"echo", "hi"}, "/tmp", os.Getenv)
|
|
if err == nil {
|
|
t.Fatal("expected error when landrun unavailable")
|
|
}
|
|
if err != ErrNotAvailable {
|
|
t.Errorf("expected ErrNotAvailable, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWrapCommand_ROX_RWX(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
roxDir := filepath.Join(tmpDir, "rox")
|
|
rwxDir := filepath.Join(tmpDir, "rwx")
|
|
os.MkdirAll(roxDir, 0755)
|
|
os.MkdirAll(rwxDir, 0755)
|
|
|
|
cfg := &Config{
|
|
Filesystem: FilesystemConfig{
|
|
ROX: []string{roxDir},
|
|
RWX: []string{rwxDir},
|
|
},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--rox", roxDir)
|
|
assertFlagValue(t, got, "--rwx", rwxDir)
|
|
}
|
|
|
|
func TestWrapCommand_SortTiebreaker(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Two paths of equal length
|
|
dirA := filepath.Join(tmpDir, "aaa")
|
|
dirB := filepath.Join(tmpDir, "bbb")
|
|
os.MkdirAll(dirA, 0755)
|
|
os.MkdirAll(dirB, 0755)
|
|
|
|
cfg := &Config{
|
|
Filesystem: FilesystemConfig{RW: []string{dirB, dirA}},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
idxA := indexOf(got, dirA)
|
|
idxB := indexOf(got, dirB)
|
|
if idxA == -1 || idxB == -1 {
|
|
t.Fatal("both dirs should be in args")
|
|
}
|
|
if idxA > idxB {
|
|
t.Errorf("dirA (%q) should sort before dirB (%q)", dirA, dirB)
|
|
}
|
|
}
|
|
|
|
func TestWrapCommand_EnvVarExpansion(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
skillsDir := filepath.Join(tmpDir, "skills")
|
|
os.MkdirAll(skillsDir, 0755)
|
|
|
|
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
|
|
|
cfg := &Config{
|
|
Filesystem: FilesystemConfig{
|
|
RWX: []string{"{XDG_CONFIG_HOME}/skills"},
|
|
},
|
|
}
|
|
got := mustWrapCommand(t, cfg, []string{"sh"}, tmpDir)
|
|
assertFlagValue(t, got, "--rwx", skillsDir)
|
|
}
|
|
|
|
// ---- helpers ----
|
|
|
|
func mustWrapCommand(t *testing.T, cfg *Config, cmd []string, cwd string) []string {
|
|
t.Helper()
|
|
got, err := WrapCommand(cfg, cmd, cwd, os.Getenv)
|
|
if err != nil {
|
|
t.Fatalf("WrapCommand failed: %v", err)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func assertContains(t *testing.T, args []string, s string) {
|
|
t.Helper()
|
|
if indexOf(args, s) == -1 {
|
|
t.Errorf("args %v does not contain %q", args, s)
|
|
}
|
|
}
|
|
|
|
func assertDoesNotContain(t *testing.T, args []string, s string) {
|
|
t.Helper()
|
|
if indexOf(args, s) != -1 {
|
|
t.Errorf("args %v should not contain %q", args, s)
|
|
}
|
|
}
|
|
|
|
func assertFlagValue(t *testing.T, args []string, flag, value string) {
|
|
t.Helper()
|
|
for i, a := range args {
|
|
if a == flag && i+1 < len(args) && args[i+1] == value {
|
|
return
|
|
}
|
|
}
|
|
t.Errorf("args %v missing %s %s", args, flag, value)
|
|
}
|
|
|
|
func indexOf(args []string, s string) int {
|
|
for i, a := range args {
|
|
if a == s {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func containsStr(ss []string, s string) bool {
|
|
for _, x := range ss {
|
|
if x == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|