This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/sandbox/sandbox_test.go
2026-03-17 16:28:47 +00:00

307 lines
7.2 KiB
Go

package sandbox
import (
"context"
"strings"
"testing"
"time"
"go.starlark.net/starlark"
)
func TestExecBasic(t *testing.T) {
sb := New(DefaultConfig())
res, err := sb.Exec(context.Background(), "test.star", `
x = 1 + 2
name = "hello"
`, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if v, ok := res.Globals["x"]; !ok {
t.Fatal("missing global 'x'")
} else if v.String() != "3" {
t.Fatalf("expected x=3, got %s", v.String())
}
if v, ok := res.Globals["name"]; !ok {
t.Fatal("missing global 'name'")
} else if v.(starlark.String).GoString() != "hello" {
t.Fatalf("expected name='hello', got %s", v.String())
}
}
func TestExecPrintCapture(t *testing.T) {
sb := New(DefaultConfig())
res, err := sb.Exec(context.Background(), "test.star", `
print("line one")
print("line two")
`, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := "line one\nline two\n"
if res.Output != expected {
t.Fatalf("expected output %q, got %q", expected, res.Output)
}
}
func TestExecStepsTracked(t *testing.T) {
sb := New(DefaultConfig())
res, err := sb.Exec(context.Background(), "test.star", `
def work():
x = 0
for i in range(100):
x += i
return x
result = work()
`, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if res.Steps == 0 {
t.Fatal("expected steps > 0")
}
}
func TestExecStepLimitExceeded(t *testing.T) {
sb := New(Config{MaxSteps: 100, Name: "limited"})
_, err := sb.Exec(context.Background(), "test.star", `
def work():
x = 0
for i in range(10000):
x += i
return x
result = work()
`, nil)
if err == nil {
t.Fatal("expected step limit error")
}
// Error message varies: "Starlark computation cancelled: exceeded ..."
// or "context canceled" — just verify it failed.
}
func TestExecContextTimeout(t *testing.T) {
// Use step limit of 0 (unlimited) so only context timeout triggers
sb := New(Config{MaxSteps: 0, Name: "timeout"})
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
_, err := sb.Exec(ctx, "test.star", `
def work():
x = 0
for i in range(100000000):
x += i
return x
result = work()
`, nil)
if err == nil {
t.Fatal("expected timeout error")
}
}
func TestExecLoadDisabled(t *testing.T) {
sb := New(DefaultConfig())
_, err := sb.Exec(context.Background(), "test.star", `
load("other.star", "foo")
`, nil)
if err == nil {
t.Fatal("expected load error")
}
if !strings.Contains(err.Error(), "disabled") {
t.Fatalf("expected 'disabled' in error, got: %v", err)
}
}
func TestExecModuleInjection(t *testing.T) {
getFn := starlark.NewBuiltin("test.get", func(
thread *starlark.Thread, b *starlark.Builtin,
args starlark.Tuple, kwargs []starlark.Tuple,
) (starlark.Value, error) {
return starlark.String("secret_value"), nil
})
mod := MakeModule("test", starlark.StringDict{
"get": getFn,
})
sb := New(DefaultConfig())
res, err := sb.Exec(context.Background(), "test.star", `
result = test.get()
`, map[string]starlark.Value{"test": mod})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
v, ok := res.Globals["result"]
if !ok {
t.Fatal("missing global 'result'")
}
if v.(starlark.String).GoString() != "secret_value" {
t.Fatalf("expected 'secret_value', got %s", v.String())
}
}
func TestExecSyntaxError(t *testing.T) {
sb := New(DefaultConfig())
_, err := sb.Exec(context.Background(), "bad.star", `
def broken(
`, nil)
if err == nil {
t.Fatal("expected syntax error")
}
se, ok := err.(*SandboxError)
if !ok {
t.Fatalf("expected *SandboxError, got %T", err)
}
_ = se // syntax errors may not have backtrace
}
func TestExecRuntimeError(t *testing.T) {
sb := New(DefaultConfig())
_, err := sb.Exec(context.Background(), "runtime.star", `
x = 1 / 0
`, nil)
if err == nil {
t.Fatal("expected runtime error")
}
se, ok := err.(*SandboxError)
if !ok {
t.Fatalf("expected *SandboxError, got %T", err)
}
if se.Backtrace == "" {
t.Fatal("expected backtrace for runtime error")
}
}
func TestCallFunction(t *testing.T) {
sb := New(DefaultConfig())
res, err := sb.Exec(context.Background(), "funcs.star", `
def greet(name):
return "hello, " + name
`, nil)
if err != nil {
t.Fatalf("exec error: %v", err)
}
fn := res.Globals["greet"].(starlark.Callable)
val, _, err := sb.Call(context.Background(), fn,
starlark.Tuple{starlark.String("world")}, nil)
if err != nil {
t.Fatalf("call error: %v", err)
}
if val.(starlark.String).GoString() != "hello, world" {
t.Fatalf("expected 'hello, world', got %s", val.String())
}
}
func TestCallWithPrint(t *testing.T) {
sb := New(DefaultConfig())
res, err := sb.Exec(context.Background(), "funcs.star", `
def run():
print("running")
return 42
`, nil)
if err != nil {
t.Fatalf("exec error: %v", err)
}
fn := res.Globals["run"].(starlark.Callable)
val, output, err := sb.Call(context.Background(), fn, nil, nil)
if err != nil {
t.Fatalf("call error: %v", err)
}
if val.String() != "42" {
t.Fatalf("expected 42, got %s", val.String())
}
if !strings.Contains(output, "running") {
t.Fatalf("expected 'running' in output, got %q", output)
}
}
func TestCallRuntimeError(t *testing.T) {
sb := New(DefaultConfig())
res, err := sb.Exec(context.Background(), "funcs.star", `
def boom():
return 1 / 0
`, nil)
if err != nil {
t.Fatalf("exec error: %v", err)
}
fn := res.Globals["boom"].(starlark.Callable)
_, _, err = sb.Call(context.Background(), fn, nil, nil)
if err == nil {
t.Fatal("expected runtime error from Call")
}
se, ok := err.(*SandboxError)
if !ok {
t.Fatalf("expected *SandboxError, got %T", err)
}
if se.Backtrace == "" {
t.Fatal("expected backtrace")
}
}
func TestNewDefaults(t *testing.T) {
sb := New(Config{})
if sb.config.MaxSteps != 0 {
t.Fatalf("expected MaxSteps=0 (unlimited), got %d", sb.config.MaxSteps)
}
if sb.config.Name != "extension" {
t.Fatalf("expected default Name='extension', got %q", sb.config.Name)
}
}
func TestNewWithDefaultConfig(t *testing.T) {
sb := New(DefaultConfig())
if sb.config.MaxSteps != 1_000_000 {
t.Fatalf("expected MaxSteps=1000000, got %d", sb.config.MaxSteps)
}
}
func TestMakeModule(t *testing.T) {
mod := MakeModule("mymod", starlark.StringDict{
"version": starlark.String("1.0"),
})
if mod.Name != "mymod" {
t.Fatalf("expected name 'mymod', got %q", mod.Name)
}
v, ok := mod.Members["version"]
if !ok {
t.Fatal("missing 'version' member")
}
if v.(starlark.String).GoString() != "1.0" {
t.Fatalf("expected '1.0', got %s", v.String())
}
}
func TestSandboxErrorUnwrap(t *testing.T) {
inner := starlark.String("test").Truth() // just need any value
_ = inner
se := &SandboxError{
Err: context.DeadlineExceeded,
Backtrace: "",
}
if se.Unwrap() != context.DeadlineExceeded {
t.Fatal("Unwrap should return inner error")
}
if se.Error() != "context deadline exceeded" {
t.Fatalf("unexpected error string: %q", se.Error())
}
}
func TestSandboxErrorWithBacktrace(t *testing.T) {
se := &SandboxError{
Err: context.DeadlineExceeded,
Backtrace: " funcs.star:3:5: in boom",
}
if !strings.Contains(se.Error(), "context deadline exceeded") {
t.Fatal("should contain original error")
}
if !strings.Contains(se.Error(), "funcs.star:3:5") {
t.Fatal("should contain backtrace")
}
}