internal/policy/exec_test.go (view raw)
1package policy
2
3import (
4 "log"
5 "os"
6 "path"
7 "runtime"
8 "testing"
9
10 "postern/internal/db"
11
12 "go.starlark.net/starlark"
13)
14
15func TestExecModule(t *testing.T) {
16 if runtime.GOOS == "windows" {
17 t.Skip("POSIX-specific test")
18 }
19
20 posternDB, err := db.OpenDB(&db.Config{
21 DBPath: path.Join(t.TempDir(), "meta.db"),
22 })
23 if err != nil {
24 t.Fatal(err)
25 }
26
27 defer func(posternDB *db.DB) {
28 err := posternDB.Close()
29 if err != nil {
30 log.Fatal(err)
31 }
32 }(posternDB)
33
34 policy := `
35def init():
36 return None
37
38def test_success():
39 res = exec.pipe_to_process("/bin/sh", args=["-c", "echo hello"])
40 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
41
42def test_input_piping():
43 res = exec.pipe_to_process("/bin/cat", input_data="ping")
44 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
45
46def test_args():
47 res = exec.pipe_to_process("/bin/sh", args=["-c", "echo $1", "_", "arg1"])
48 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
49
50def test_nonzero_exit():
51 res = exec.pipe_to_process("/bin/sh", args=["-c", "exit 42"])
52 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
53
54def test_stderr():
55 res = exec.pipe_to_process("/bin/sh", args=["-c", "echo err >&2"])
56 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
57
58def test_environment():
59 res = exec.pipe_to_process("/bin/sh", args=["-c", "echo $FOO"], environment={"FOO": "bar"})
60 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
61
62def test_env_no_leak():
63 res = exec.pipe_to_process("/bin/sh", args=["-c", "echo ${HOME:-empty}"])
64 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
65
66def test_timeout():
67 res = exec.pipe_to_process("/bin/sh", args=["-c", "sleep 10"], timeout=1)
68 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
69
70def test_timeout_float():
71 res = exec.pipe_to_process("/bin/sh", args=["-c", "sleep 1"], timeout=0.5)
72 return {"code": res.code, "stdout": res.stdout, "stderr": res.stderr, "timed_out": res.timed_out}
73
74def test_not_found():
75 return exec.pipe_to_process("/nonexistent/binary/xyz")
76`
77
78 policyFile := path.Join(t.TempDir(), "policy.star")
79 err = os.WriteFile(policyFile, []byte(policy), 0644)
80 if err != nil {
81 t.Fatal(err)
82 }
83
84 e, err := NewEngine(policyFile, posternDB)
85 if err != nil {
86 t.Fatal(err)
87 }
88 s := e.state.Load()
89
90 tests := []struct {
91 name string
92 fnName string
93 wantCode int
94 wantOut string
95 wantErr string
96 wantTimeout bool
97 }{
98 {"success", "test_success", 0, "hello\n", "", false},
99 {"input_piping", "test_input_piping", 0, "ping", "", false},
100 {"args", "test_args", 0, "arg1\n", "", false},
101 {"nonzero_exit", "test_nonzero_exit", 42, "", "", false},
102 {"stderr", "test_stderr", 0, "", "err\n", false},
103 {"environment", "test_environment", 0, "bar\n", "", false},
104 {"env_no_leak", "test_env_no_leak", 0, "empty\n", "", false},
105 {"timeout", "test_timeout", -1, "", "", true},
106 {"timeout_float", "test_timeout_float", -1, "", "", true},
107 }
108
109 for _, tt := range tests {
110 t.Run(tt.name, func(t *testing.T) {
111 hookVal, ok := s.globals[tt.fnName]
112 if !ok {
113 t.Fatalf("%s not found in engine globals", tt.fnName)
114 }
115
116 res, err := starlark.Call(s.thread, hookVal, nil, nil)
117 if err != nil {
118 t.Fatal(err)
119 }
120
121 dict, ok := res.(*starlark.Dict)
122 if !ok {
123 t.Fatalf("expected dict, got %s", res.Type())
124 }
125
126 assertDictField(t, dict, "code", starlark.MakeInt(tt.wantCode))
127 assertDictField(t, dict, "stdout", starlark.String(tt.wantOut))
128 assertDictField(t, dict, "stderr", starlark.String(tt.wantErr))
129 assertDictField(t, dict, "timed_out", starlark.Bool(tt.wantTimeout))
130 })
131 }
132
133 t.Run("not_found", func(t *testing.T) {
134 hookVal, ok := s.globals["test_not_found"]
135 if !ok {
136 t.Fatal("test_not_found not found in engine globals")
137 }
138 _, err := starlark.Call(s.thread, hookVal, nil, nil)
139 if err == nil {
140 t.Fatal("expected error for nonexistent binary")
141 }
142 })
143}
144
145func assertDictField(t *testing.T, d *starlark.Dict, key string, want starlark.Value) {
146 t.Helper()
147 v, found, err := d.Get(starlark.String(key))
148 if err != nil {
149 t.Fatalf("key %q err %q", key, err)
150 }
151 if !found {
152 t.Fatalf("key %q not found", key)
153 }
154 if v != want {
155 t.Errorf("field %q: got %v, want %v", key, v, want)
156 }
157}