92 lines
3.3 KiB
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()
|
|
}
|
|
}
|