package file import ( "context" "os" "path/filepath" "testing" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/tools" ) func TestNewReadFile_DenyByDefault(t *testing.T) { tool := NewReadFile(config.FileOpsCfg{AllowedPaths: []string{}}) result := tool.Exec(context.Background(), map[string]any{"path": "/etc/hosts"}) if result.Err == nil { t.Fatal("expected error when AllowedPaths is empty, got nil") } } func TestNewReadFile_AllowedPath(t *testing.T) { tmp := t.TempDir() f := filepath.Join(tmp, "test.txt") os.WriteFile(f, []byte("hello"), 0644) tool := NewReadFile(config.FileOpsCfg{AllowedPaths: []string{tmp}}) result := tool.Exec(context.Background(), map[string]any{"path": f}) if result.Err != nil { t.Fatalf("expected success, got: %v", result.Err) } if result.Output != "hello" { t.Fatalf("expected 'hello', got %q", result.Output) } } func TestNewReadFile_PathTraversal(t *testing.T) { tmp := t.TempDir() // Try to escape via ../ tool := NewReadFile(config.FileOpsCfg{AllowedPaths: []string{tmp}}) result := tool.Exec(context.Background(), map[string]any{ "path": filepath.Join(tmp, "..", "..", "etc", "hosts"), }) if result.Err == nil { t.Fatal("expected error for path traversal, got nil") } } func TestNewReadFile_PathOutsideAllowed(t *testing.T) { tmp := t.TempDir() tool := NewReadFile(config.FileOpsCfg{AllowedPaths: []string{tmp}}) result := tool.Exec(context.Background(), map[string]any{"path": "/etc/hosts"}) if result.Err == nil { t.Fatal("expected error for path outside allowed, got nil") } } func TestNewReadFile_SymlinkEscape(t *testing.T) { tmp := t.TempDir() link := filepath.Join(tmp, "escape") os.Symlink("/etc", link) tool := NewReadFile(config.FileOpsCfg{AllowedPaths: []string{tmp}}) result := tool.Exec(context.Background(), map[string]any{ "path": filepath.Join(link, "hosts"), }) if result.Err == nil { t.Fatal("expected error for symlink escape, got nil") } } func TestNewReadFile_EmptyPath(t *testing.T) { tool := NewReadFile(config.FileOpsCfg{AllowedPaths: []string{"/tmp"}}) result := tool.Exec(context.Background(), map[string]any{"path": ""}) if result.Err == nil { t.Fatal("expected error for empty path") } } func TestNewReadFile_PrefixConfusion(t *testing.T) { // /opt should not match /opt1234 tool := NewReadFile(config.FileOpsCfg{AllowedPaths: []string{"/opt"}}) result := tool.Exec(context.Background(), map[string]any{"path": "/opt1234/file.txt"}) if result.Err == nil { t.Fatal("expected error: /opt should not match /opt1234") } } func TestValidatePath_ExactMatch(t *testing.T) { tmp := t.TempDir() if err := validatePath(tmp, []string{tmp}); err != nil { t.Fatalf("exact match should be allowed: %v", err) } } func TestGetString_MissingKey(t *testing.T) { val := tools.GetString(map[string]any{}, "missing") if val != "" { t.Fatalf("expected empty, got %q", val) } }