byteclaw/internal/tools/tools_test.go

92 lines
3.3 KiB
Go

package tools
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestTimeNow(t *testing.T) {
fixed := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
out, err := TimeNow{Now: func() time.Time { return fixed }}.Call(context.Background(), json.RawMessage(`{"timezone":"Europe/Berlin"}`))
if err != nil || !strings.HasPrefix(out, "2026-01-02T04:04:05") {
t.Fatalf("%q %v", out, err)
}
if _, err := (TimeNow{}).Call(context.Background(), json.RawMessage(`{"timezone":"Nope/Nope"}`)); err == nil {
t.Fatal("expected tz error")
}
}
func TestWorkspaceConfinement(t *testing.T) {
dir := t.TempDir()
ws := Workspace{Dir: filepath.Join(dir, "ws"), MaxBytes: 100}
ctx := context.Background()
w, r, l := FileWrite{ws}, FileRead{ws}, FileList{ws}
if _, err := w.Call(ctx, json.RawMessage(`{"path":"a/b.txt","content":"hi"}`)); err != nil {
t.Fatal(err)
}
if out, err := r.Call(ctx, json.RawMessage(`{"path":"a/b.txt"}`)); err != nil || out != "hi" {
t.Fatalf("%q %v", out, err)
}
if out, _ := l.Call(ctx, json.RawMessage(`{}`)); out != "a/\n" {
t.Fatalf("list %q", out)
}
os.WriteFile(filepath.Join(dir, "secret"), []byte("x"), 0o600)
os.Symlink(filepath.Join(dir, "secret"), filepath.Join(ws.Dir, "link"))
for _, p := range []string{"../secret", "/etc/passwd", "a/../../secret", "link"} {
if _, err := r.Call(ctx, json.RawMessage(fmt.Sprintf(`{"path":%q}`, p))); err == nil {
t.Errorf("read of %q should fail", p)
}
if _, err := w.Call(ctx, json.RawMessage(fmt.Sprintf(`{"path":%q,"content":"x"}`, p))); err == nil {
t.Errorf("write of %q should fail", p)
}
}
if _, err := w.Call(ctx, json.RawMessage(`{"path":"big","content":"`+strings.Repeat("x", 101)+`"}`)); err == nil {
t.Error("size limit not enforced")
}
}
func TestWebFetchBlocksPrivateByDefault(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "ok") }))
defer srv.Close()
_, err := NewWebFetch(2*time.Second, 1024, false).Call(context.Background(), json.RawMessage(`{"url":"`+srv.URL+`"}`))
if err == nil || !errors.Is(err, ErrBlockedAddr) {
t.Fatalf("expected blocked, got %v", err)
}
if _, err := NewWebFetch(time.Second, 10, false).Call(context.Background(), json.RawMessage(`{"url":"file:///etc/passwd"}`)); err == nil {
t.Fatal("file scheme must be rejected")
}
}
func TestWebFetchStripsAndTruncates(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, "<html><script>evil()</script><p>Hello <b>World</b></p>"+strings.Repeat("y", 500)+"</html>")
}))
defer srv.Close()
out, err := NewWebFetch(2*time.Second, 80, true).Call(context.Background(), json.RawMessage(`{"url":"`+srv.URL+`"}`))
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "evil") || !strings.Contains(out, "Hello World") || !strings.HasSuffix(out, "[truncated]") {
t.Fatalf("unexpected: %q", out)
}
}
func TestRegistryUnknownAndBadJSON(t *testing.T) {
r := NewRegistry(TimeNow{})
if !strings.Contains(r.Call(context.Background(), "nope", "{}"), "unknown tool") {
t.Fatal()
}
if !strings.Contains(r.Call(context.Background(), "time_now", "{bad"), "not valid JSON") {
t.Fatal()
}
}